// 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() if sif, ok := st.In.(*os.File); ok { os.Stdin = sif } else { fmt.Printf("In is not an *os.File: %#v\n", st.In) } os.Stdout = st.Out.(*os.File) os.Stderr = st.Err.(*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 } _, ok := rw.(io.Writer) if !ok { 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() } for len(st.PipeIn) > 0 { CloseReader(st.PipeIn.Pop()) } } // 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 { if o == nil { return nil } cur := st.GetWrapped() if cur == nil { return nil } 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 { _, ok := st.Out.(*WriteWrapper) if !ok { return nil } 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, 131, 132} // KnownN is the highest valid value for type Known, plus one. const KnownN Known = 133 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, `Goal`: 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, `SQL`: 51, `Tcl`: 52, `AnyDoc`: 53, `BibTeX`: 54, `TeX`: 55, `Texinfo`: 56, `Troff`: 57, `Html`: 58, `Css`: 59, `Markdown`: 60, `Rtf`: 61, `MSWord`: 62, `OpenText`: 63, `OpenPres`: 64, `MSPowerpoint`: 65, `EBook`: 66, `EPub`: 67, `AnySheet`: 68, `MSExcel`: 69, `OpenSheet`: 70, `AnyData`: 71, `Csv`: 72, `Json`: 73, `Xml`: 74, `Protobuf`: 75, `Ini`: 76, `Tsv`: 77, `Uri`: 78, `Color`: 79, `Yaml`: 80, `Toml`: 81, `Number`: 82, `String`: 83, `Tensor`: 84, `Table`: 85, `AnyText`: 86, `PlainText`: 87, `ICal`: 88, `VCal`: 89, `VCard`: 90, `AnyImage`: 91, `Pdf`: 92, `Postscript`: 93, `Gimp`: 94, `GraphVis`: 95, `Gif`: 96, `Jpeg`: 97, `Png`: 98, `Svg`: 99, `Tiff`: 100, `Pnm`: 101, `Pbm`: 102, `Pgm`: 103, `Ppm`: 104, `Xbm`: 105, `Xpm`: 106, `Bmp`: 107, `Heic`: 108, `Heif`: 109, `AnyModel`: 110, `Vrml`: 111, `X3d`: 112, `Obj`: 113, `AnyAudio`: 114, `Aac`: 115, `Flac`: 116, `Mp3`: 117, `Ogg`: 118, `Midi`: 119, `Wav`: 120, `AnyVideo`: 121, `Mpeg`: 122, `Mp4`: 123, `Mov`: 124, `Ogv`: 125, `Wmv`: 126, `Avi`: 127, `AnyFont`: 128, `TrueType`: 129, `WebOpenFont`: 130, `AnyExe`: 131, `AnyBin`: 132} 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: ``, 52: ``, 53: `Doc is an editable word processing file including latex, markdown, html, css, etc`, 54: ``, 55: ``, 56: ``, 57: ``, 58: ``, 59: ``, 60: ``, 61: ``, 62: ``, 63: ``, 64: ``, 65: ``, 66: ``, 67: ``, 68: `Sheet is a spreadsheet file (.xls etc)`, 69: ``, 70: ``, 71: `Data is some kind of data format (csv, json, database, etc)`, 72: ``, 73: ``, 74: ``, 75: ``, 76: ``, 77: ``, 78: ``, 79: ``, 80: ``, 81: ``, 82: `special support for data fs`, 83: ``, 84: ``, 85: ``, 86: `Text is some other kind of text file`, 87: ``, 88: ``, 89: ``, 90: ``, 91: `Image is an image (jpeg, png, svg, etc) *including* PDF`, 92: ``, 93: ``, 94: ``, 95: ``, 96: ``, 97: ``, 98: ``, 99: ``, 100: ``, 101: ``, 102: ``, 103: ``, 104: ``, 105: ``, 106: ``, 107: ``, 108: ``, 109: ``, 110: `Model is a 3D model`, 111: ``, 112: ``, 113: ``, 114: `Audio is an audio file`, 115: ``, 116: ``, 117: ``, 118: ``, 119: ``, 120: ``, 121: `Video is a video file`, 122: ``, 123: ``, 124: ``, 125: ``, 126: ``, 127: ``, 128: `Font is a font file`, 129: ``, 130: ``, 131: `Exe is a binary executable file`, 132: `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: `Goal`, 32: `Haskell`, 33: `Java`, 34: `JavaScript`, 35: `Lisp`, 36: `Lua`, 37: `Makefile`, 38: `Mathematica`, 39: `Matlab`, 40: `ObjC`, 41: `OCaml`, 42: `Pascal`, 43: `Perl`, 44: `Php`, 45: `Prolog`, 46: `Python`, 47: `R`, 48: `Ruby`, 49: `Rust`, 50: `Scala`, 51: `SQL`, 52: `Tcl`, 53: `AnyDoc`, 54: `BibTeX`, 55: `TeX`, 56: `Texinfo`, 57: `Troff`, 58: `Html`, 59: `Css`, 60: `Markdown`, 61: `Rtf`, 62: `MSWord`, 63: `OpenText`, 64: `OpenPres`, 65: `MSPowerpoint`, 66: `EBook`, 67: `EPub`, 68: `AnySheet`, 69: `MSExcel`, 70: `OpenSheet`, 71: `AnyData`, 72: `Csv`, 73: `Json`, 74: `Xml`, 75: `Protobuf`, 76: `Ini`, 77: `Tsv`, 78: `Uri`, 79: `Color`, 80: `Yaml`, 81: `Toml`, 82: `Number`, 83: `String`, 84: `Tensor`, 85: `Table`, 86: `AnyText`, 87: `PlainText`, 88: `ICal`, 89: `VCal`, 90: `VCard`, 91: `AnyImage`, 92: `Pdf`, 93: `Postscript`, 94: `Gimp`, 95: `GraphVis`, 96: `Gif`, 97: `Jpeg`, 98: `Png`, 99: `Svg`, 100: `Tiff`, 101: `Pnm`, 102: `Pbm`, 103: `Pgm`, 104: `Ppm`, 105: `Xbm`, 106: `Xpm`, 107: `Bmp`, 108: `Heic`, 109: `Heif`, 110: `AnyModel`, 111: `Vrml`, 112: `X3d`, 113: `Obj`, 114: `AnyAudio`, 115: `Aac`, 116: `Flac`, 117: `Mp3`, 118: `Ogg`, 119: `Midi`, 120: `Wav`, 121: `AnyVideo`, 122: `Mpeg`, 123: `Mp4`, 124: `Mov`, 125: `Ogv`, 126: `Wmv`, 127: `Avi`, 128: `AnyFont`, 129: `TrueType`, 130: `WebOpenFont`, 131: `AnyExe`, 132: `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/fs" "log" "os" "path" "path/filepath" "strings" "testing" "time" "cogentcore.org/core/base/datasize" "cogentcore.org/core/base/fsx" "cogentcore.org/core/base/vcs" "cogentcore.org/core/icons" "github.com/Bios-Marcel/wastebasket/v2" ) // 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:"-"` // Generated indicates that the file is generated and should not be edited. // For Go files, this regex: `^// Code generated .* DO NOT EDIT\.$` is used. Generated bool `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 { fi.Cat = UnknownCategory fi.Known = Unknown fi.Generated = false fi.Kind = "" var errs []error path, err := filepath.Abs(fname) if err == nil { fi.Path = path } else { fi.Path = fname } _, fi.Name = filepath.Split(path) info, err := os.Stat(fi.Path) if err != nil { errs = append(errs, err) fi.MimeFromFilename() } 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 { fi.Cat = UnknownCategory fi.Known = Unknown fi.Generated = false fi.Kind = "" var errs []error fi.Path = fname _, fi.Name = path.Split(fname) info, err := fs.Stat(fsys, fi.Path) if err != nil { errs = append(errs, err) fi.MimeFromFilename() } else { fi.SetFileInfo(info) } return errors.Join(errs...) } // MimeFromFilename sets the mime data based only on the filename // without attempting to open the file. func (fi *FileInfo) MimeFromFilename() error { ext := strings.ToLower(filepath.Ext(fi.Path)) if mtype, has := ExtMimeMap[ext]; has { // only use our filename ext map fi.SetMimeFromType(mtype) return nil } return errors.New("FileInfo MimeFromFilename: Filename extension not known: " + ext) } // MimeFromFile sets the mime data for a valid file (i.e., os.Stat works). // Use MimeFromFilename to only examine the filename. func (fi *FileInfo) MimeFromFile() error { if fi.Path == "" || fi.Path == "." || fi.IsDir() { return nil } fi.Generated = IsGeneratedFile(fi.Path) mtype, _, err := MimeFromFile(fi.Path) if err != nil { return err } fi.SetMimeFromType(mtype) return nil } // SetMimeType sets file info fields from given mime type string. func (fi *FileInfo) SetMimeFromType(mtype string) { fi.Mime = mtype fi.Cat = CategoryFromMime(mtype) fi.Known = MimeKnown(mtype) if fi.Cat != UnknownCategory { fi.Kind = fi.Cat.String() + ": " } if fi.Known != Unknown { fi.Kind += fi.Known.String() } else { fi.Kind += MimeSub(fi.Mime) } } // SetFileInfo updates from given [fs.FileInfo]. It uses a canonical // [FileInfo.ModTime] when testing to ensure consistent results. func (fi *FileInfo) SetFileInfo(info fs.FileInfo) { fi.Size = datasize.Size(info.Size()) fi.Mode = info.Mode() if testing.Testing() { // We use a canonical time when testing to ensure consistent results. fi.ModTime = time.Unix(1500000000, 0) } else { fi.ModTime = info.ModTime() } if info.IsDir() { fi.Kind = "Folder" fi.Cat = Folder fi.Known = AnyFolder } else { if fi.Mode.IsRegular() { fi.MimeFromFile() } 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, fsx.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 = errors.New("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" // 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 Goal Haskell Java JavaScript Lisp Lua Makefile Mathematica Matlab ObjC OCaml Pascal Perl Php Prolog Python R Ruby Rust Scala SQL 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" "os" "path/filepath" "regexp" "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) } var generatedRe = regexp.MustCompile(`^// Code generated .* DO NOT EDIT`) func IsGeneratedFile(fname string) bool { file, err := os.Open(fname) if err != nil { return false } head := make([]byte, 2048) file.Read(head) return generatedRe.Match(head) } // 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", ".goal"}, 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" "io/fs" "os" "path/filepath" "sort" "strings" "time" ) // Filename is used to open a file picker dialog when used as an argument // type in a function, or as a field value. type Filename string // 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 DirEntry'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 } // 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) 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") } } // ExcludeFile returns true if the given file is on the exclude list. func ExcludeFile(pkg *packages.Package, file *ast.File, exclude ...string) bool { fpos := pkg.Fset.Position(file.FileStart) _, fname := filepath.Split(fpos.Filename) for _, ex := range exclude { if fname == ex { return true } } return false } // Inspect goes through all of the files in the given package, // except those listed in the exclude list, and calls the given // function on each node. 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), exclude ...string) error { for _, file := range pkg.Syntax { if ExcludeFile(pkg, file, exclude...) { 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" "errors" "fmt" "image" "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, errors.New("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. // It [Unwrap]s any [Wrapped] images. func Write(im image.Image, w io.Writer, f Formats) error { im = Unwrap(im) 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) } } // 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" "cogentcore.org/core/base/num" ) // 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, // or if the environment variable "CORE_UPDATE_TESTDATA" is set to "true". // It should typically only be set through those methods. 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 } // DiffImage returns the difference between two images, // with pixels having the abs of the difference between pixels. func DiffImage(a, b image.Image) image.Image { ab := a.Bounds() di := image.NewRGBA(ab) for y := ab.Min.Y; y < ab.Max.Y; y++ { for x := ab.Min.X; x < ab.Max.X; x++ { cc := color.RGBAModel.Convert(a.At(x, y)).(color.RGBA) ic := color.RGBAModel.Convert(b.At(x, y)).(color.RGBA) r := uint8(num.Abs(int(cc.R) - int(ic.R))) g := uint8(num.Abs(int(cc.G) - int(ic.G))) b := uint8(num.Abs(int(cc.B) - int(ic.B))) c := color.RGBA{r, g, b, 255} di.Set(x, y, c) } } return di } // 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 diffFilename := strings.TrimSuffix(filename, ext) + ".diff" + 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) } os.RemoveAll(diffFilename) 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) // TODO(#1456): reduce tolerance to 1 after we fix rendering inconsistencies if !CompareColors(cc, ic, 10) { 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) } err = Save(DiffImage(img, fimg), diffFilename) if err != nil { t.Errorf("AssertImage: error saving diff image: %v", err) } } else { err := os.RemoveAll(failFilename) if err != nil { t.Errorf("AssertImage: error removing old fail image: %v", err) } os.RemoveAll(diffFilename) } } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build !js package imagex import "image" // WrapJS returns a JavaScript optimized wrapper around the given // [image.Image] on web, and just returns the image on other platforms. func WrapJS(src image.Image) image.Image { return src } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package imagex import ( "image" "github.com/anthonynsimon/bild/clone" ) // Wrapped extends the [image.Image] interface with two methods that manage // the wrapping of an underlying Go [image.Image]. This can be used for images that // are actually GPU textures, and to manage JavaScript pointers on the js platform. type Wrapped interface { image.Image // Update is called whenever the image data has been updated, // to update any additional data based on the new image. // This may copy an image to the GPU or update JavaScript pointers. Update() // Underlying returns the underlying image.Image, which should // be called whenever passing the image to some other Go-based // function that is likely to be optimized for different image types, // such as [draw.Draw]. Do NOT use this for functions that will // directly handle the wrapped image! Underlying() image.Image } // Update calls [Wrapped.Update] on a [Wrapped] if it is one. // It does nothing otherwise. func Update(src image.Image) { if wr, ok := src.(Wrapped); ok { wr.Update() } } // Unwrap calls [Wrapped.Underlying] on a [Wrapped] if it is one. // It returns the original image otherwise. func Unwrap(src image.Image) image.Image { if wr, ok := src.(Wrapped); ok { return wr.Underlying() } return src } // CloneAsRGBA returns an [*image.RGBA] copy of the supplied image. // It calls [Unwrap] first. See also [AsRGBA]. func CloneAsRGBA(src image.Image) *image.RGBA { return clone.AsRGBA(Unwrap(src)) } // AsRGBA returns the image as an [*image.RGBA]. If it already is one, // it returns that image directly. Otherwise it returns a clone. // It calls [Unwrap] first. See also [CloneAsRGBA]. func AsRGBA(src image.Image) *image.RGBA { return clone.AsShallowRGBA(Unwrap(src)) } // 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 ( "errors" "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 errors.New("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 keylist implements an ordered list (slice) of items, with a map from a key (e.g., names) to indexes, to support fast lookup by name. This is a different implementation of the [ordmap] package, that has separate slices for Values and Keys, instead of using a tuple list of both. The awkwardness of value access through the tuple is the major problem with ordmap. */ package keylist import ( "fmt" "slices" ) // TODO: probably want to consolidate ordmap and keylist: https://github.com/cogentcore/core/issues/1224 // List implements an ordered list (slice) of Values, // with a map from a key (e.g., names) to indexes, // to support fast lookup by name. type List[K comparable, V any] struct { //types:add // List is the ordered slice of items. Values []V // Keys is the ordered list of keys, in same order as [List.Values] Keys []K // indexes is the key-to-index mapping. indexes map[K]int } // New returns a new [List]. The zero value // is usable without initialization, so this is // just a simple standard convenience method. func New[K comparable, V any]() *List[K, V] { return &List[K, V]{} } func (kl *List[K, V]) makeIndexes() { kl.indexes = make(map[K]int) } // initIndexes ensures that the index map exists. func (kl *List[K, V]) initIndexes() { if kl.indexes == nil { kl.makeIndexes() } } // Reset resets the list, removing any existing elements. func (kl *List[K, V]) Reset() { kl.Values = nil kl.Keys = nil kl.makeIndexes() } // Set sets given key to given value, adding to the end of the list // if not already present, and otherwise replacing with this new value. // This is the same semantics as a Go map. // See [List.Add] for version that only adds and does not replace. func (kl *List[K, V]) Set(key K, val V) { kl.initIndexes() if idx, ok := kl.indexes[key]; ok { kl.Values[idx] = val kl.Keys[idx] = key return } kl.indexes[key] = len(kl.Values) kl.Values = append(kl.Values, val) kl.Keys = append(kl.Keys, key) } // Add adds an item to the list with given key, // An error is returned if the key is already on the list. // See [List.Set] for a method that automatically replaces. func (kl *List[K, V]) Add(key K, val V) error { kl.initIndexes() if _, ok := kl.indexes[key]; ok { return fmt.Errorf("keylist.Add: key %v is already on the list", key) } kl.indexes[key] = len(kl.Values) kl.Values = append(kl.Values, val) kl.Keys = append(kl.Keys, key) return nil } // Insert inserts the given value with the given key at the given index. // This is relatively slow because it needs regenerate the keys list. // It panics if the key already exists because the behavior is undefined // in that situation. func (kl *List[K, V]) Insert(idx int, key K, val V) { if _, has := kl.indexes[key]; has { panic("keylist.Add: key is already on the list") } kl.Keys = slices.Insert(kl.Keys, idx, key) kl.Values = slices.Insert(kl.Values, idx, val) kl.makeIndexes() for i, k := range kl.Keys { kl.indexes[k] = i } } // At returns the value corresponding to the given key, // with a zero value returned for a missing key. See [List.AtTry] // for one that returns a bool for missing keys. // For index-based access, use [List.Values] or [List.Keys] slices directly. func (kl *List[K, V]) At(key K) V { idx, ok := kl.indexes[key] if ok { return kl.Values[idx] } var zv V return zv } // AtTry returns the value corresponding to the given key, // with false returned for a missing key, in case the zero value // is not diagnostic. func (kl *List[K, V]) AtTry(key K) (V, bool) { idx, ok := kl.indexes[key] if ok { return kl.Values[idx], true } var zv V return zv, false } // IndexIsValid returns an error if the given index is invalid. func (kl *List[K, V]) IndexIsValid(idx int) error { if idx >= len(kl.Values) || idx < 0 { return fmt.Errorf("keylist.List: IndexIsValid: index %d is out of range of a list of length %d", idx, len(kl.Values)) } return nil } // IndexByKey returns the index of the given key, with a -1 for missing key. func (kl *List[K, V]) IndexByKey(key K) int { idx, ok := kl.indexes[key] if !ok { return -1 } return idx } // Len returns the number of items in the list. func (kl *List[K, V]) Len() int { if kl == nil { return 0 } return len(kl.Values) } // DeleteByIndex deletes item(s) within the index range [i:j]. // This is relatively slow because it needs to regenerate the // index map. func (kl *List[K, V]) DeleteByIndex(i, j int) { ndel := j - i if ndel <= 0 { panic("index range is <= 0") } kl.Keys = slices.Delete(kl.Keys, i, j) kl.Values = slices.Delete(kl.Values, i, j) kl.makeIndexes() for i, k := range kl.Keys { kl.indexes[k] = i } } // DeleteByKey deletes the item with the given key, // returning false if it does not find it. // This is relatively slow because it needs to regenerate the // index map. func (kl *List[K, V]) DeleteByKey(key K) bool { idx, ok := kl.indexes[key] if !ok { return false } kl.DeleteByIndex(idx, idx+1) return true } // RenameIndex renames the item at given index to new key. func (kl *List[K, V]) RenameIndex(i int, key K) { old := kl.Keys[i] delete(kl.indexes, old) kl.Keys[i] = key kl.indexes[key] = i } // Copy copies all of the entries from the given key list // into this list. It keeps existing entries in this // list unless they also exist in the given list, in which case // they are overwritten. Use [List.Reset] first to get an exact copy. func (kl *List[K, V]) Copy(from *List[K, V]) { for i, v := range from.Values { kl.Set(kl.Keys[i], v) } } // String returns a string representation of the list. func (kl *List[K, V]) String() string { sv := "{" for i, v := range kl.Values { sv += fmt.Sprintf("%v", kl.Keys[i]) + ": " + fmt.Sprintf("%v", v) + ", " } sv += "}" return sv } // UpdateIndexes updates the Indexes from Keys and Values. // This must be called after loading Values from a file, for example, // where Keys can be populated from Values or are also otherwise available. func (kl *List[K, V]) UpdateIndexes() { kl.makeIndexes() for i := range kl.Values { k := kl.Keys[i] kl.indexes[k] = i } } /* // GoString returns the list as Go code. func (kl *List[K, V]) GoString() string { var zk K var zv V res := fmt.Sprintf("ordlist.Make([]ordlist.KeyVal[%T, %T]{\n", zk, zv) for _, kv := range kl.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 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" } 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", "err", 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 provides a map of named any elements // with generic support for type-safe Get and nil-safe Set. // Metadata keys often function as optional fields in a struct, // and therefore a CamelCase naming convention is typical. // Provides default support for "Name", "Doc", "File" standard keys. package metadata import ( "fmt" "maps" "cogentcore.org/core/base/errors" ) // Data is metadata as a map of named any elements // with generic support for type-safe Get and nil-safe Set. // Metadata keys often function as optional fields in a struct, // and therefore a CamelCase naming convention is typical. // Provides default support for "Name" and "Doc" standard keys. // In general it is good practice to provide access functions // that establish standard key names, to avoid issues with typos. 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 } // GetFromData gets metadata value of given type from given Data. // Returns error if not present or item is a different type. func GetFromData[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) } //////// Metadataer // Metadataer is an interface for a type that returns associated // metadata.Data using a Metadata() method. To be able to set metadata, // the method should be defined with a pointer receiver. type Metadataer interface { Metadata() *Data } // GetData gets the Data from given object, if it implements the // Metadata() method. Returns nil if it does not. // Must pass a pointer to the object. func GetData(obj any) *Data { if md, ok := obj.(Metadataer); ok { return md.Metadata() } return nil } // Get gets metadata value of given type from given object, // if it implements the Metadata() method. // Must pass a pointer to the object. // Returns error if not present or item is a different type. func Get[T any](obj any, key string) (T, error) { md := GetData(obj) if md == nil { var zv T return zv, errors.New("metadata not available for given object type") } return GetFromData[T](*md, key) } // Set sets metadata value on given object, if it implements // the Metadata() method. Returns error if no Metadata on object. // Must pass a pointer to the object. func Set(obj any, key string, value any) error { md := GetData(obj) if md == nil { return errors.Log(errors.New("metadata not available for given object type")) } md.Set(key, value) return nil } // Copy copies metadata from source // Must pass a pointer to the object. func Copy(to, src any) *Data { tod := GetData(to) if tod == nil { return nil } srcd := GetData(src) if srcd == nil { return tod } tod.Copy(*srcd) return tod } // 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 "os" // SetName sets the "Name" standard key. func SetName(obj any, name string) { Set(obj, "Name", name) } // Name returns the "Name" standard key value (empty if not set). func Name(obj any) string { nm, _ := Get[string](obj, "Name") return nm } // SetDoc sets the "Doc" standard key. func SetDoc(obj any, doc string) { Set(obj, "Doc", doc) } // Doc returns the "Doc" standard key value (empty if not set). func Doc(obj any) string { doc, _ := Get[string](obj, "Doc") return doc } // SetFile sets the "File" standard key for *os.File. func SetFile(obj any, file *os.File) { Set(obj, "File", file) } // File returns the "File" standard key value (nil if not set). func File(obj any) *os.File { doc, _ := Get[*os.File](obj, "File") return doc } // SetFilename sets the "Filename" standard key. func SetFilename(obj any, file string) { Set(obj, "Filename", file) } // Filename returns the "Filename" standard key value (empty if not set). func Filename(obj any) string { doc, _ := Get[string](obj, "Filename") return doc } // 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) 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 } 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: it would be good to return an ordmap or struct of the fields for // ordered output, but that may be difficult. // 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))) } // FieldByPath returns the [reflect.Value] of given field within given struct value, // where the field can be a path with . separators, for fields within struct fields. func FieldByPath(s reflect.Value, fieldPath string) (reflect.Value, error) { sv := Underlying(s) if sv.Kind() != reflect.Struct { return reflect.Value{}, errors.New("reflectx.FieldByPath: kind is not struct") } fps := strings.Split(fieldPath, ".") for _, fp := range fps { fv := sv.FieldByName(fp) if !fv.IsValid() { return reflect.Value{}, errors.New("reflectx.FieldByPath: field name not found: " + fp) } sv = fv } return sv, nil } // CopyFields copies the named fields from src struct into dest struct. // Fields can be paths with . separators for sub-fields of fields. func CopyFields(dest, src any, fields ...string) error { dsv := Underlying(reflect.ValueOf(dest)) if dsv.Kind() != reflect.Struct { return errors.New("reflectx.CopyFields: destination kind is not struct") } ssv := Underlying(reflect.ValueOf(src)) if ssv.Kind() != reflect.Struct { return errors.New("reflectx.CopyFields: source kind is not struct") } var errs []error for _, f := range fields { dfv, err := FieldByPath(dsv, f) if err != nil { errs = append(errs, err) continue } sfv, err := FieldByPath(ssv, f) if err != nil { errs = append(errs, err) continue } err = SetRobust(PointerValue(dfv).Interface(), sfv.Interface()) if err != nil { errs = append(errs, err) continue } } return errors.Join(errs...) } // SetFieldsFromMap sets given map[string]any values to fields of given object, // where the map keys are field paths (with . delimiters for sub-field paths). // The value can be any appropriate type that applies to the given field. // It prints a message if a parameter fails to be set, and returns an error. func SetFieldsFromMap(obj any, vals map[string]any) error { objv := reflect.ValueOf(obj) npv := NonPointerValue(objv) if npv.Kind() == reflect.Map { err := CopyMapRobust(obj, vals) if errors.Log(err) != nil { return err } } var errs []error for k, v := range vals { fld, err := FieldByPath(objv, k) if err != nil { errs = append(errs, err) } err = SetRobust(PointerValue(fld).Interface(), v) if err != nil { err = errors.Log(fmt.Errorf("SetFieldsFromMap: was not able to apply value: %v to field: %s", v, k)) errs = append(errs, err) } } 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 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/base/errors" "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 } // KindIsInt returns whether the given [reflect.Kind] is an int // type such as int, int32 etc. func KindIsInt(vk reflect.Kind) bool { return vk >= reflect.Int && vk <= reflect.Uintptr } // KindIsFloat returns whether the given [reflect.Kind] is a // float32 or float64. func KindIsFloat(vk reflect.Kind) bool { return vk >= reflect.Float32 && vk <= reflect.Float64 } // 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, errors.New("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, errors.New("got nil *int") } return *vt != 0, nil case int32: return vt != 0, nil case *int32: if vt == nil { return false, errors.New("got nil *int32") } return *vt != 0, nil case int64: return vt != 0, nil case *int64: if vt == nil { return false, errors.New("got nil *int64") } return *vt != 0, nil case uint8: return vt != 0, nil case *uint8: if vt == nil { return false, errors.New("got nil *uint8") } return *vt != 0, nil case float64: return vt != 0, nil case *float64: if vt == nil { return false, errors.New("got nil *float64") } return *vt != 0, nil case float32: return vt != 0, nil case *float32: if vt == nil { return false, errors.New("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, errors.New("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, errors.New("got nil *int8") } return *vt != 0, nil case int16: return vt != 0, nil case *int16: if vt == nil { return false, errors.New("got nil *int16") } return *vt != 0, nil case uint: return vt != 0, nil case *uint: if vt == nil { return false, errors.New("got nil *uint") } return *vt != 0, nil case uint16: return vt != 0, nil case *uint16: if vt == nil { return false, errors.New("got nil *uint16") } return *vt != 0, nil case uint32: return vt != 0, nil case *uint32: if vt == nil { return false, errors.New("got nil *uint32") } return *vt != 0, nil case uint64: return vt != 0, nil case *uint64: if vt == nil { return false, errors.New("got nil *uint64") } return *vt != 0, nil case uintptr: return vt != 0, nil case *uintptr: if vt == nil { return false, errors.New("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, errors.New("got nil *int") } return int64(*vt), nil case int32: return int64(vt), nil case *int32: if vt == nil { return 0, errors.New("got nil *int32") } return int64(*vt), nil case int64: return vt, nil case *int64: if vt == nil { return 0, errors.New("got nil *int64") } return *vt, nil case uint8: return int64(vt), nil case *uint8: if vt == nil { return 0, errors.New("got nil *uint8") } return int64(*vt), nil case float64: return int64(vt), nil case *float64: if vt == nil { return 0, errors.New("got nil *float64") } return int64(*vt), nil case float32: return int64(vt), nil case *float32: if vt == nil { return 0, errors.New("got nil *float32") } return int64(*vt), nil case bool: if vt { return 1, nil } return 0, nil case *bool: if vt == nil { return 0, errors.New("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, errors.New("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, errors.New("got nil *int8") } return int64(*vt), nil case int16: return int64(vt), nil case *int16: if vt == nil { return 0, errors.New("got nil *int16") } return int64(*vt), nil case uint: return int64(vt), nil case *uint: if vt == nil { return 0, errors.New("got nil *uint") } return int64(*vt), nil case uint16: return int64(vt), nil case *uint16: if vt == nil { return 0, errors.New("got nil *uint16") } return int64(*vt), nil case uint32: return int64(vt), nil case *uint32: if vt == nil { return 0, errors.New("got nil *uint32") } return int64(*vt), nil case uint64: return int64(vt), nil case *uint64: if vt == nil { return 0, errors.New("got nil *uint64") } return int64(*vt), nil case uintptr: return int64(vt), nil case *uintptr: if vt == nil { return 0, errors.New("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, errors.New("got nil *float64") } return *vt, nil case float32: return float64(vt), nil case *float32: if vt == nil { return 0, errors.New("got nil *float32") } return float64(*vt), nil case int: return float64(vt), nil case *int: if vt == nil { return 0, errors.New("got nil *int") } return float64(*vt), nil case int32: return float64(vt), nil case *int32: if vt == nil { return 0, errors.New("got nil *int32") } return float64(*vt), nil case int64: return float64(vt), nil case *int64: if vt == nil { return 0, errors.New("got nil *int64") } return float64(*vt), nil case uint8: return float64(vt), nil case *uint8: if vt == nil { return 0, errors.New("got nil *uint8") } return float64(*vt), nil case bool: if vt { return 1, nil } return 0, nil case *bool: if vt == nil { return 0, errors.New("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, errors.New("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, errors.New("got nil *int8") } return float64(*vt), nil case int16: return float64(vt), nil case *int16: if vt == nil { return 0, errors.New("got nil *int16") } return float64(*vt), nil case uint: return float64(vt), nil case *uint: if vt == nil { return 0, errors.New("got nil *uint") } return float64(*vt), nil case uint16: return float64(vt), nil case *uint16: if vt == nil { return 0, errors.New("got nil *uint16") } return float64(*vt), nil case uint32: return float64(vt), nil case *uint32: if vt == nil { return 0, errors.New("got nil *uint32") } return float64(*vt), nil case uint64: return float64(vt), nil case *uint64: if vt == nil { return 0, errors.New("got nil *uint64") } return float64(*vt), nil case uintptr: return float64(vt), nil case *uintptr: if vt == nil { return 0, errors.New("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, errors.New("got nil *float32") } return *vt, nil case float64: return float32(vt), nil case *float64: if vt == nil { return 0, errors.New("got nil *float64") } return float32(*vt), nil case int: return float32(vt), nil case *int: if vt == nil { return 0, errors.New("got nil *int") } return float32(*vt), nil case int32: return float32(vt), nil case *int32: if vt == nil { return 0, errors.New("got nil *int32") } return float32(*vt), nil case int64: return float32(vt), nil case *int64: if vt == nil { return 0, errors.New("got nil *int64") } return float32(*vt), nil case uint8: return float32(vt), nil case *uint8: if vt == nil { return 0, errors.New("got nil *uint8") } return float32(*vt), nil case bool: if vt { return 1, nil } return 0, nil case *bool: if vt == nil { return 0, errors.New("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, errors.New("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, errors.New("got nil *int8") } return float32(*vt), nil case int16: return float32(vt), nil case *int16: if vt == nil { return 0, errors.New("got nil *int8") } return float32(*vt), nil case uint: return float32(vt), nil case *uint: if vt == nil { return 0, errors.New("got nil *uint") } return float32(*vt), nil case uint16: return float32(vt), nil case *uint16: if vt == nil { return 0, errors.New("got nil *uint16") } return float32(*vt), nil case uint32: return float32(vt), nil case *uint32: if vt == nil { return 0, errors.New("got nil *uint32") } return float32(*vt), nil case uint64: return float32(vt), nil case *uint64: if vt == nil { return 0, errors.New("got nil *uint64") } return float32(*vt), nil case uintptr: return float32(vt), nil case *uintptr: if vt == nil { return 0, errors.New("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) { // If the original value is a non-nil pointer, we can just use it // even though the underlying pointer is nil (this happens when there // is a pointer to a nil pointer; see #1365). if !IsNil(rto) && rto.Kind() == reflect.Pointer { pto = rto } else { // Otherwise, we cannot recover any meaningful value. return errors.New("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) } // images should not be copied per content: just set the pointer! // otherwise the original images (esp colors!) are altered. // TODO: #1394 notes the more general ambiguity about deep vs. shallow pointer copy. if img, ok := to.(*image.Image); ok { if fimg, ok := from.(image.Image); ok { *img = fimg return nil } } // 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) 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" "errors" "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 errors.New("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, errors.New("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 } switch s { case "!", "?": return "$" + s } return os.Getenv(s) } 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) { for k, v := range cl.Env { ses.Setenv(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" "slices" "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 } } // UniqueList removes duplicates from given string list, // preserving the order. func UniqueList(strs []string) []string { n := len(strs) for i := n - 1; i >= 0; i-- { p := strs[i] for j, s := range strs { if p == s && i != j { strs = slices.Delete(strs, i, i+1) } } } return strs } // 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" "sync" "github.com/Masterminds/vcs" ) type GitRepo struct { vcs.GitRepo files Files gettingFiles bool sync.Mutex } func (gr *GitRepo) Type() Types { return Git } // Files returns a map of the current files and their status, // using a cached version of the file list if available. // nil will be returned immediately if no cache is available. // The given onUpdated function will be called from a separate // goroutine when the updated list of the files is available, // if an update is not already under way. An update is always triggered // if no files have yet been cached, even if the function is nil. func (gr *GitRepo) Files(onUpdated func(f Files)) (Files, error) { gr.Lock() if gr.files != nil { f := gr.files gr.Unlock() if onUpdated != nil { go gr.updateFiles(onUpdated) } return f, nil } gr.Unlock() go gr.updateFiles(onUpdated) return nil, nil } func (gr *GitRepo) updateFiles(onUpdated func(f Files)) { gr.Lock() if gr.gettingFiles { gr.Unlock() return } gr.gettingFiles = true gr.Unlock() nf := max(len(gr.files), 64) f := make(Files, nf) out, err := gr.RunFromDir("git", "ls-files", "-o") // other -- untracked if err == nil { 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 { 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 { 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 { 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 { 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 { scan := bufio.NewScanner(bytes.NewReader(out)) for scan.Scan() { fn := filepath.FromSlash(string(scan.Bytes())) f[fn] = Added } } gr.Lock() gr.files = f gr.Unlock() if onUpdated != nil { onUpdated(f) } gr.Lock() gr.gettingFiles = false gr.Unlock() } 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 } // StatusFast returns file status based on the cached file info, // which might be slightly stale. Much faster than Status. // Returns Untracked if no cached files. func (gr *GitRepo) StatusFast(fname string) FileStatus { var ff Files gr.Lock() ff = gr.files gr.Unlock() if ff != nil { return ff.Status(gr, fname) } 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 { fname = relPath(gr, fname) out, err := gr.RunFromDir("git", "add", fname) if err != nil { log.Println(string(out)) return err } gr.Lock() if gr.files != nil { gr.files[fname] = Added } gr.Unlock() 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" "sync" "github.com/Masterminds/vcs" ) type SvnRepo struct { vcs.SvnRepo files Files gettingFiles bool sync.Mutex } 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 } } // Files returns a map of the current files and their status, // using a cached version of the file list if available. // nil will be returned immediately if no cache is available. // The given onUpdated function will be called from a separate // goroutine when the updated list of the files is available, // if an update is not already under way. An update is always triggered // if no files have yet been cached, even if the function is nil. func (gr *SvnRepo) Files(onUpdated func(f Files)) (Files, error) { gr.Lock() if gr.files != nil { f := gr.files gr.Unlock() if onUpdated != nil { go gr.updateFiles(onUpdated) } return f, nil } gr.Unlock() go gr.updateFiles(onUpdated) return nil, nil } func (gr *SvnRepo) updateFiles(onUpdated func(f Files)) { gr.Lock() if gr.gettingFiles { gr.Unlock() return } gr.gettingFiles = true gr.Unlock() f := make(Files) lpath := gr.LocalPath() allfs, err := allFiles(lpath) // much faster than svn list --recursive if err == nil { for _, fn := range allfs { rpath, _ := filepath.Rel(lpath, fn) f[rpath] = Stored } } out, err := gr.RunFromDir("svn", "status", "-u") if err == nil { 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) } } gr.Lock() gr.files = f gr.gettingFiles = false gr.Unlock() if onUpdated != nil { onUpdated(f) } } // StatusFast returns file status based on the cached file info, // which might be slightly stale. Much faster than Status. // Returns Untracked if no cached files. func (gr *SvnRepo) StatusFast(fname string) FileStatus { var ff Files gr.Lock() ff = gr.files gr.Unlock() if ff != nil { return ff.Status(gr, fname) } return Untracked } // 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 } gr.Lock() if gr.files != nil { gr.files[fname] = Added } gr.Unlock() 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 ( "errors" "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, // using a cached version of the file list if available. // nil will be returned immediately if no cache is available. // The given onUpdated function will be called from a separate // goroutine when the updated list of the files is available // (an update is always triggered even if the function is nil). Files(onUpdated func(f Files)) (Files, error) // StatusFast returns file status based on the cached file info, // which might be slightly stale. Much faster than Status. // Returns Untracked if no cached files. StatusFast(fname string) FileStatus // 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 = errors.New("hg version control not yet supported") case vcs.Bzr: err = errors.New("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 } // 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 } 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 } if len(cmds) == 1 { // one command is always the root cs[0].Root = true } 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 { logx.PrintlnDebug("loaded config file:", fn) 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) if !f.IsExported() { continue } 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 ( "errors" "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 errors.New("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 ( "errors" "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 errors.New("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 ( "errors" "fmt" "os" "path/filepath" "runtime" "strings" "cogentcore.org/core/base/exec" "cogentcore.org/core/base/logx" "cogentcore.org/core/cmd/core/config" "github.com/mitchellh/go-homedir" ) // 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": for _, ld := range linuxDistros { _, err := exec.LookPath(ld.tool) if err != nil { continue // package manager not found } cmd, args := ld.cmd() err = vc.Run(cmd, args...) if err != nil { return err // package installation failed } return nil // success } return errors.New("unknown Linux distro; please file a bug report at https://github.com/cogentcore/core/issues") case "windows": // We must be in the home directory to avoid permission issues with file downloading. hd, err := homedir.Dir() if err != nil { return err } err = os.Chdir(hd) if err != nil { return err } 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) } // linuxDistro represents the data needed to install dependencies for a specific Linux // distribution family with the same installation steps. type linuxDistro struct { // name contains the user-friendly name(s) of the Linux distribution(s). name string // sudo is whether the package manager requires sudo. sudo bool // tool is the name of the package manager used for installation. tool string // command contains the subcommand(s) in the package manager used to install packages. command []string // packages are the packages that need to be installed. packages []string } // cmd returns the command and arguments to install the packages for the Linux distribution. func (ld *linuxDistro) cmd() (cmd string, args []string) { if ld.sudo { cmd = "sudo" args = append(args, ld.tool) } else { cmd = ld.tool } args = append(args, ld.command...) args = append(args, ld.packages...) return } func (ld *linuxDistro) String() string { cmd, args := ld.cmd() return fmt.Sprintf("%-15s %s %s", ld.name+":", cmd, strings.Join(args, " ")) } // linuxDistros contains the supported Linux distributions, // based on https://docs.fyne.io/started. var linuxDistros = []*linuxDistro{ {name: "Debian/Ubuntu", sudo: true, tool: "apt", command: []string{"install"}, packages: []string{ "gcc", "libgl1-mesa-dev", "libegl1-mesa-dev", "mesa-vulkan-drivers", "xorg-dev", }}, {name: "Fedora", sudo: true, tool: "dnf", command: []string{"install"}, packages: []string{ "gcc", "libX11-devel", "libXcursor-devel", "libXrandr-devel", "libXinerama-devel", "mesa-libGL-devel", "libXi-devel", "libXxf86vm-devel", "mesa-vulkan-drivers", }}, {name: "Arch", sudo: true, tool: "pacman", command: []string{"-S"}, packages: []string{ "xorg-server-devel", "libxcursor", "libxrandr", "libxinerama", "libxi", "vulkan-swrast", }}, {name: "Solus", sudo: true, tool: "eopkg", command: []string{"it", "-c"}, packages: []string{ "system.devel", "mesalib-devel", "libxrandr-devel", "libxcursor-devel", "libxi-devel", "libxinerama-devel", "vulkan", }}, {name: "openSUSE", sudo: true, tool: "zypper", command: []string{"install"}, packages: []string{ "gcc", "libXcursor-devel", "libXrandr-devel", "Mesa-libGL-devel", "libXi-devel", "libXinerama-devel", "libXxf86vm-devel", "libvulkan1", }}, {name: "Void", sudo: true, tool: "xbps-install", command: []string{"-S"}, packages: []string{ "base-devel", "xorg-server-devel", "libXrandr-devel", "libXcursor-devel", "libXinerama-devel", "vulkan-loader", }}, {name: "Alpine", sudo: true, tool: "apk", command: []string{"add"}, packages: []string{ "gcc", "libxcursor-dev", "libxrandr-dev", "libxinerama-dev", "libxi-dev", "linux-headers", "mesa-dev", "vulkan-loader", }}, {name: "NixOS", sudo: false, tool: "nix-shell", command: []string{"-p"}, packages: []string{ "libGL", "pkg-config", "xorg.libX11.dev", "xorg.libXcursor", "xorg.libXi", "xorg.libXinerama", "xorg.libXrandr", "xorg.libXxf86vm", "mesa.drivers", "vulkan-loader", }}, } // 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:"-"` // Content, if specified, indicates that the app has core content pages // located at this directory. If so, 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 content. Content 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 = 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 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" "errors" "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, errors.New("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, errors.New("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, errors.New("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, errors.New("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, errors.New("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, errors.New("pool already exists") } bx.Pool = new(Pool) return bx.Pool, nil case ResXMLResourceMap: if bx.Map != nil { return nil, errors.New("resource map already exists") } bx.Map = new(Map) return bx.Map, nil case ResXMLStartNamespace: if bx.Namespace != nil { return nil, errors.New("namespace start already exists") } bx.Namespace = new(Namespace) return bx.Namespace, nil case ResXMLEndNamespace: if bx.Namespace.end != nil { return nil, errors.New("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, errors.New("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, errors.New("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 ( "errors" "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, errors.New("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" "errors" "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, errors.New("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 errors.New("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, errors.New("cannot build non-main package") } if c.ID == "" { return nil, errors.New("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" "errors" "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 = errors.New("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" } 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/base/iox/imagex" "cogentcore.org/core/icons" "cogentcore.org/core/math32" _ "cogentcore.org/core/paint/renderers" "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) { sv := svg.NewSVG(math32.Vec2(float32(size), float32(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 } } return imagex.AsRGBA(sv.RenderImage()), 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" "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/content/bcontent" 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 } } prindex := &bcontent.PreRenderPage{ HTML: preRenderHTML, } prps := []*bcontent.PreRenderPage{} if strings.HasPrefix(preRenderHTML, "[{") { err := jsonx.Read(&prps, strings.NewReader(preRenderHTML)) if err != nil { return err } if c.Content == "" { c.Content = "content" } } for _, prp := range prps { if prp.URL == "" { prindex = prp break } } prindex.Name = c.Name if c.About != "" { prindex.Description = c.About } iht, err := makeIndexHTML(c, "", prindex) if err != nil { return err } err = os.WriteFile(filepath.Join(odir, "index.html"), iht, 0666) if err != nil { return err } // The 404 page is just the same as the index page, with an updated base path. // The logic in the home page can then handle the error appropriately. bpath404 := "../" // TODO: this is a temporary hack to fix the 404 page for multi-nested old URLs in the Cogent Core Docs. if c.Name == "Cogent Core Docs" { if c.Build.Trimpath { bpath404 = "https://www.cogentcore.org/core/" // production } else { bpath404 = "http://localhost:8080/" // dev } } notFound, err := makeIndexHTML(c, bpath404, prindex) if err != nil { return err } err = os.WriteFile(filepath.Join(odir, "404.html"), notFound, 0666) if err != nil { return nil } if c.Content != "" { err := makePages(c, prps) 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, prps []*bcontent.PreRenderPage) error { for _, prp := range prps { if prp.URL == "" { // exclude root index (already handled) continue } opath := filepath.Join(c.Build.Output, prp.URL) err := os.MkdirAll(opath, 0777) if err != nil { return err } b, err := makeIndexHTML(c, "../", prp) if err != nil { return err } err = os.WriteFile(filepath.Join(opath, "index.html"), b, 0666) if err != nil { return 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 web import ( "net/http" "os" "path/filepath" "strings" "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 { hfs := http.FileServer(http.Dir(c.Build.Output)) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { trim := strings.Trim(r.URL.Path, "/") _, err := os.Stat(filepath.Join(c.Build.Output, trim)) if err != nil { r.URL.Path = "/404.html" trim = "404.html" w.WriteHeader(http.StatusNotFound) } if trim == "app.wasm" { w.Header().Set("Content-Type", "application/wasm") if c.Web.Gzip { w.Header().Set("Content-Encoding", "gzip") } } hfs.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" "cogentcore.org/core/content/bcontent" 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 Styles []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 string, prp *bcontent.PreRenderPage) ([]byte, error) { if prp.Description == "" { prp.Description = c.About } else { // c.About is already stripped earlier, so only necessary // for page-specific description here. prp.Description = strip.StripTags(prp.Description) } d := indexHTMLData{ BasePath: basePath, Author: c.Web.Author, Description: prp.Description, Keywords: c.Web.Keywords, Title: prp.Name, SiteName: c.Name, Image: c.Web.Image, Styles: c.Web.Styles, VanityURL: c.Web.VanityURL, GithubVanityRepository: c.Web.GithubVanityRepository, PreRenderHTML: prp.HTML, } 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) } 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) } 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) } return math32.Vec3(-1.0, -1.0, -1.0) } 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) } 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 } 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) } 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 // computed current render versions transformed by object matrix rCenter math32.Vector2 rFocal math32.Vector2 rRadius math32.Vector2 } 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. // See [ToUniform] for the converse. func Uniform(c color.Color) image.Image { return image.NewUniform(c) } // ToUniform converts the given image to a uniform [color.RGBA] color. // See [Uniform] for the converse. 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 { // blue, red, green, yellow, violet, aqua, orange, blueviolet // hues := []float32{30, 280, 140, 110, 330, 200, 70, 305} hues := []float32{255, 25, 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 { // blue, red, green, yellow, violet, aqua, orange, blueviolet // hues := []float32{30, 280, 140, 110, 330, 200, 70, 305} hues := []float32{255, 25, 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) 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 bcontent ("base content") provides base types and functions // shared by both content and the core build tool for content. This is // necessary to ensure that the core build tool does not import GUI packages. package bcontent import ( "bufio" "bytes" "fmt" "io/fs" "path/filepath" "slices" "strconv" "strings" "time" "cogentcore.org/core/base/iox/tomlx" "cogentcore.org/core/base/strcase" ) // Page represents the metadata for a single page of content. type Page struct { // Source is the filesystem that the page is stored in. Source fs.FS `toml:"-" json:"-"` // Filename is the name of the file in [Page.FS] that the content is stored in. Filename string `toml:"-" json:"-"` // Name is the user-friendly name of the page, defaulting to the // [strcase.ToSentence] of the [Page.Filename] without its extension. Name string // URL is the URL of the page relative to the root of the app, without // any leading slash. It defaults to [Page.Name] in kebab-case // (ex: "home" or "text-fields"). A blank URL ("") manually // specified in the front matter indicates that this the root page. URL string // Title is the title displayed at the top of the page. It defaults to [Page.Name]. // Note that [Page.Name] is still used for the stage title and other such things; this // is only for the actual title widget. Title string // Date is the optional date that the page was published. Date time.Time // Authors are the optional authors of the page. Authors []string // Draft indicates that the page is a draft and should not be visible on the web. Draft bool // Categories are the categories that the page belongs to. Categories []string // Specials are special content elements for each page // that have names with an underscore-delimited key name, // such as figure_, table_, sim_ etc, and can be referred // to using the #id component of a wikilink. They are rendered // using the index of each such element (e.g., Figure 1) in the link. Specials map[string][]string } // PreRenderPage contains the data for each page printed in JSON by a content app // run with the generatehtml tag, which is then handled by the core // build tool. type PreRenderPage struct { Page // Description is the automatic page description. Description string // HTML is the pre-rendered HTML for the page. HTML string } // NewPage makes a new page in the given filesystem with the given filename, // sets default values, and reads metadata from the front matter of the page file. func NewPage(source fs.FS, filename string) (*Page, error) { pg := &Page{Source: source, Filename: filename} pg.Defaults() err := pg.ReadMetadata() return pg, err } // Defaults sets default values for the page based on its filename. func (pg *Page) Defaults() { pg.Name = strcase.ToSentence(strings.TrimSuffix(pg.Filename, filepath.Ext(pg.Filename))) pg.URL = strcase.ToKebab(pg.Name) pg.Title = pg.Name } // ReadMetadata reads the page metadata from the front matter of the page file, // if there is any. func (pg *Page) ReadMetadata() error { f, err := pg.Source.Open(pg.Filename) if err != nil { return err } defer f.Close() sc := bufio.NewScanner(f) var data []byte for sc.Scan() { b := sc.Bytes() if data == nil { if string(b) != `+++` { return nil } data = []byte{} continue } if string(b) == `+++` { break } data = append(data, append(b, '\n')...) } return tomlx.ReadBytes(pg, data) } // ReadContent returns the page content with any front matter removed. // It also applies [Page.categoryLinks]. func (pg *Page) ReadContent(pagesByCategory map[string][]*Page) ([]byte, error) { b, err := fs.ReadFile(pg.Source, pg.Filename) if err != nil { return nil, err } b = append(b, pg.categoryLinks(pagesByCategory)...) if !bytes.HasPrefix(b, []byte(`+++`)) { return b, nil } b = bytes.TrimPrefix(b, []byte(`+++`)) _, after, has := bytes.Cut(b, []byte(`+++`)) if !has { return nil, fmt.Errorf("unclosed front matter") } return after, nil } // categoryLinks, if the page has the same names as one of the given categories, // returns markdown containing a list of links to all pages in that category. // Otherwise, it returns nil. func (pg *Page) categoryLinks(pagesByCategory map[string][]*Page) []byte { if pagesByCategory == nil { return nil } cpages := pagesByCategory[pg.Name] if cpages == nil { return nil } res := []byte{'\n'} for _, cpage := range cpages { res = append(res, fmt.Sprintf("* [[%s]]\n", cpage.Name)...) } return res } // SpecialName extracts a special element type name from given element name, // defined as the part before the first underscore _ character. func SpecialName(name string) string { usi := strings.Index(name, "_") if usi < 0 { return "" } return name[:usi] } // SpecialToKebab does strcase.ToKebab on parts after specialName if present. func SpecialToKebab(name string) string { usi := strings.Index(name, "_") if usi < 0 { return strcase.ToKebab(name) } spec := name[:usi+1] name = name[usi+1:] colon := strings.Index(name, ":") if colon > 0 { return spec + strcase.ToKebab(name[:colon]) + name[colon:] } else { return spec + strcase.ToKebab(name) } } // SpecialLabel returns the label for given special element, using // the index of the element in the list of specials, e.g., "Figure 1" func (pg *Page) SpecialLabel(name string) string { snm := SpecialName(name) if snm == "" { return "" } if pg.Specials == nil { b, err := pg.ReadContent(nil) if err != nil { return "" } pg.ParseSpecials(b) } sl := pg.Specials[snm] if sl == nil { return "" } i := slices.Index(sl, name) if i < 0 { return "" } return strcase.ToSentence(snm) + " " + strconv.Itoa(i+1) } // ParseSpecials manually parses specials before rendering md // because they are needed in advance of generating from md file, // e.g., for wikilinks. func (pg *Page) ParseSpecials(b []byte) { if pg.Specials != nil { return } pg.Specials = make(map[string][]string) scan := bufio.NewScanner(bytes.NewReader(b)) idt := []byte(`{id="`) idn := len(idt) for scan.Scan() { ln := scan.Bytes() n := len(ln) if n < idn+1 { continue } if !bytes.HasPrefix(ln, idt) { continue } fs := bytes.Fields(ln) // multiple attributes possible ln = fs[0] // only deal with first one id := bytes.TrimSpace(ln[idn:]) n = len(id) if n < 2 { continue } ed := n - 1 // quotes if len(fs) == 1 { ed = n - 2 // brace } id = id[:ed] sid := string(id) snm := SpecialName(sid) if snm == "" { continue } // fmt.Println("id:", snm, sid) sl := pg.Specials[snm] sl = append(sl, sid) pg.Specials[snm] = sl } } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package content import ( "slices" "cogentcore.org/core/colors" "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/icons" "cogentcore.org/core/keymap" "cogentcore.org/core/styles" "cogentcore.org/core/system" "cogentcore.org/core/tree" ) func (ct *Content) MakeToolbar(p *tree.Plan) { if false && ct.SizeClass() == core.SizeCompact { // TODO: implement hamburger menu for compact tree.Add(p, func(w *core.Button) { w.SetIcon(icons.Menu) w.SetTooltip("Navigate pages and headings") w.OnClick(func(e events.Event) { d := core.NewBody("Navigate") // tree.MoveToParent(ct.leftFrame, d) d.AddBottomBar(func(bar *core.Frame) { d.AddCancel(bar) }) d.RunDialog(w) }) }) } tree.Add(p, func(w *core.Button) { w.SetIcon(icons.Icon(core.AppIcon)) w.SetTooltip("Home") w.OnClick(func(e events.Event) { ct.Open("") }) }) // Superseded by browser navigation on web. if core.TheApp.Platform() != system.Web { tree.Add(p, func(w *core.Button) { w.SetIcon(icons.ArrowBack).SetKey(keymap.HistPrev) w.SetTooltip("Back") w.Updater(func() { w.SetEnabled(ct.historyIndex > 0) }) w.OnClick(func(e events.Event) { ct.historyIndex-- ct.open(ct.history[ct.historyIndex].URL, false) // do not add to history while navigating history }) }) tree.Add(p, func(w *core.Button) { w.SetIcon(icons.ArrowForward).SetKey(keymap.HistNext) w.SetTooltip("Forward") w.Updater(func() { w.SetEnabled(ct.historyIndex < len(ct.history)-1) }) w.OnClick(func(e events.Event) { ct.historyIndex++ ct.open(ct.history[ct.historyIndex].URL, false) // do not add to history while navigating history }) }) } tree.Add(p, func(w *core.Button) { w.SetText("Search").SetIcon(icons.Search).SetKey(keymap.Menu) w.Styler(func(s *styles.Style) { s.Background = colors.Scheme.SurfaceVariant s.Padding.Right.Em(5) }) w.OnClick(func(e events.Event) { ct.Scene.MenuSearchDialog("Search", "Search "+core.TheApp.Name()) }) }) } func (ct *Content) MenuSearch(items *[]core.ChooserItem) { newItems := make([]core.ChooserItem, len(ct.pages)) for i, pg := range ct.pages { newItems[i] = core.ChooserItem{ Value: pg, Text: pg.Name, Icon: icons.Article, Func: func() { ct.Open(pg.URL) }, } } *items = append(newItems, *items...) } // makeBottomButtons makes the previous and next buttons if relevant. func (ct *Content) makeBottomButtons(p *tree.Plan) { if len(ct.currentPage.Categories) == 0 { return } cat := ct.currentPage.Categories[0] pages := ct.pagesByCategory[cat] idx := slices.Index(pages, ct.currentPage) ct.prevPage, ct.nextPage = nil, nil if idx > 0 { ct.prevPage = pages[idx-1] } if idx < len(pages)-1 { ct.nextPage = pages[idx+1] } if ct.prevPage == nil && ct.nextPage == nil { return } tree.Add(p, func(w *core.Frame) { w.Styler(func(s *styles.Style) { s.Align.Items = styles.Center s.Grow.Set(1, 0) }) w.Maker(func(p *tree.Plan) { if ct.prevPage != nil { tree.Add(p, func(w *core.Button) { w.SetText("Previous").SetIcon(icons.ArrowBack).SetType(core.ButtonTonal) ct.Context.LinkButtonUpdating(w, func() string { // needed to prevent stale URL variable return ct.prevPage.URL }) }) } if ct.nextPage != nil { tree.Add(p, func(w *core.Stretch) {}) tree.Add(p, func(w *core.Button) { w.SetText("Next").SetIcon(icons.ArrowForward).SetType(core.ButtonTonal) ct.Context.LinkButtonUpdating(w, func() string { // needed to prevent stale URL variable return ct.nextPage.URL }) }) } }) }) } // 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 content provides a system for making content-focused // apps and websites consisting of Markdown, HTML, and Cogent Core. package content //go:generate core generate import ( "bytes" "cmp" "fmt" "io" "io/fs" "net/http" "path/filepath" "slices" "strconv" "strings" "github.com/gomarkdown/markdown/ast" "golang.org/x/exp/maps" "cogentcore.org/core/base/errors" "cogentcore.org/core/base/fsx" "cogentcore.org/core/base/strcase" "cogentcore.org/core/content/bcontent" "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/htmlcore" "cogentcore.org/core/math32" "cogentcore.org/core/styles" "cogentcore.org/core/styles/units" "cogentcore.org/core/system" "cogentcore.org/core/text/csl" "cogentcore.org/core/tree" ) // Content manages and displays the content of a set of pages. type Content struct { core.Splits // Source is the source filesystem for the content. // It should be set using [Content.SetSource] or [Content.SetContent]. Source fs.FS `set:"-"` // Context is the [htmlcore.Context] used to render the content, // which can be modified for things such as adding wikilink handlers. Context *htmlcore.Context `set:"-"` // References is a list of references used for generating citation text // for literature reference wikilinks in the format [[@CiteKey]]. References *csl.KeyList // pages are the pages that constitute the content. pages []*bcontent.Page // pagesByName has the [bcontent.Page] for each [bcontent.Page.Name] // transformed into lowercase. See [Content.pageByName] for a helper // function that automatically transforms into lowercase. pagesByName map[string]*bcontent.Page // pagesByURL has the [bcontent.Page] for each [bcontent.Page.URL]. pagesByURL map[string]*bcontent.Page // pagesByCategory has the [bcontent.Page]s for each of all [bcontent.Page.Categories]. pagesByCategory map[string][]*bcontent.Page // categories has all unique [bcontent.Page.Categories], sorted such that the categories // with the most pages are listed first. categories []string // history is the history of pages that have been visited. // The oldest page is first. history []*bcontent.Page // historyIndex is the current position in [Content.history]. historyIndex int // currentPage is the currently open page. currentPage *bcontent.Page // renderedPage is the most recently rendered page. renderedPage *bcontent.Page // leftFrame is the frame on the left side of the widget, // used for displaying the table of contents and the categories. leftFrame *core.Frame // rightFrame is the frame on the right side of the widget, // used for displaying the page content. rightFrame *core.Frame // tocNodes are all of the tree nodes in the table of contents // by kebab-case heading name. tocNodes map[string]*core.Tree // currentHeading is the currently selected heading in the table of contents, // if any (in kebab-case). currentHeading string // The previous and next page, if applicable. They must be stored on this struct // to avoid stale local closure variables. prevPage, nextPage *bcontent.Page } func init() { // We want Command+[ and Command+] to work for browser back/forward navigation // in content, since we rely on that. They should still be intercepted by // Cogent Core for non-content apps for things such as full window dialogs, // so we only add these in content. system.ReservedWebShortcuts = append(system.ReservedWebShortcuts, "Command+[", "Command+]") } func (ct *Content) Init() { ct.Splits.Init() ct.SetSplits(0.2, 0.8) ct.Context = htmlcore.NewContext() ct.Context.OpenURL = func(url string) { ct.Open(url) } ct.Context.GetURL = func(url string) (*http.Response, error) { return htmlcore.GetURLFromFS(ct.Source, url) } ct.Context.AddWikilinkHandler(ct.citeWikilink) ct.Context.AddWikilinkHandler(ct.mainWikilink) ct.Context.ElementHandlers["embed-page"] = func(ctx *htmlcore.Context) bool { errors.Log(ct.embedPage(ctx)) return true } ct.Context.AttributeHandlers["id"] = func(ctx *htmlcore.Context, w io.Writer, node ast.Node, entering bool, tag, value string) bool { if ct.currentPage == nil { return false } lbl := ct.currentPage.SpecialLabel(value) ch := node.GetChildren() if len(ch) == 2 { // image or table if entering { sty := htmlcore.MDGetAttr(node, "style") if sty != "" { if img, ok := ch[1].(*ast.Image); ok { htmlcore.MDSetAttr(img, "style", sty) delete(node.AsContainer().Attribute.Attrs, "style") } } return false } cp := "\n<p><b>" + lbl + ":</b>" if img, ok := ch[1].(*ast.Image); ok { // fmt.Printf("Image: %s\n", string(img.Destination)) // fmt.Printf("Image: %#v\n", img) nc := len(img.Children) if nc > 0 { if txt, ok := img.Children[0].(*ast.Text); ok { // fmt.Printf("text: %s\n", string(txt.Literal)) // not formatted! cp += " " + string(txt.Literal) // todo: not formatted! } } } else { title := htmlcore.MDGetAttr(node, "title") if title != "" { cp += " " + title } } cp += "</p>\n" w.Write([]byte(cp)) } else if entering { cp := "\n<span id=\"" + value + "\"><b>" + lbl + ":</b>" title := htmlcore.MDGetAttr(node, "title") if title != "" { cp += " " + title } cp += "</span>\n" w.Write([]byte(cp)) // fmt.Println("id:", value, lbl) // fmt.Printf("%#v\n", node) } return false } ct.Maker(func(p *tree.Plan) { if ct.currentPage == nil { return } tree.Add(p, func(w *core.Frame) { ct.leftFrame = w }) tree.Add(p, func(w *core.Frame) { ct.rightFrame = w w.Styler(func(s *styles.Style) { switch w.SizeClass() { case core.SizeCompact, core.SizeMedium: s.Padding.SetHorizontal(units.Em(0.5)) case core.SizeExpanded: s.Padding.SetHorizontal(units.Em(3)) } }) w.Maker(func(p *tree.Plan) { if ct.currentPage.Title != "" { tree.Add(p, func(w *core.Text) { w.SetType(core.TextDisplaySmall) w.Updater(func() { w.SetText(ct.currentPage.Title) }) }) } tree.Add(p, func(w *core.Frame) { w.Styler(func(s *styles.Style) { s.Direction = styles.Column s.Grow.Set(1, 1) }) w.Updater(func() { errors.Log(ct.loadPage(w)) }) }) ct.makeBottomButtons(p) }) }) }) // Must be done after the default title is set elsewhere in normal OnShow ct.OnFinal(events.Show, func(e events.Event) { ct.setStageTitle() }) ct.handleWebPopState() } // pageByName returns [Content.pagesByName] of the lowercase version of the given name. func (ct *Content) pageByName(name string) *bcontent.Page { ln := strings.ToLower(name) if pg, ok := ct.pagesByName[ln]; ok { return pg } nd := strings.ReplaceAll(ln, "-", " ") if pg, ok := ct.pagesByName[nd]; ok { return pg } return nil } // SetSource sets the source filesystem for the content. func (ct *Content) SetSource(source fs.FS) *Content { ct.Source = source ct.pages = []*bcontent.Page{} ct.pagesByName = map[string]*bcontent.Page{} ct.pagesByURL = map[string]*bcontent.Page{} ct.pagesByCategory = map[string][]*bcontent.Page{} errors.Log(fs.WalkDir(ct.Source, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } if path == "" || path == "." { return nil } ext := filepath.Ext(path) if !(ext == ".md" || ext == ".html") { return nil } pg, err := bcontent.NewPage(ct.Source, path) if err != nil { return err } ct.pages = append(ct.pages, pg) ct.pagesByName[strings.ToLower(pg.Name)] = pg ct.pagesByURL[pg.URL] = pg for _, cat := range pg.Categories { ct.pagesByCategory[cat] = append(ct.pagesByCategory[cat], pg) } return nil })) ct.categories = maps.Keys(ct.pagesByCategory) slices.SortFunc(ct.categories, func(a, b string) int { v := cmp.Compare(len(ct.pagesByCategory[b]), len(ct.pagesByCategory[a])) if v != 0 { return v } return cmp.Compare(a, b) }) if url := ct.getWebURL(); url != "" { ct.Open(url) return ct } if root, ok := ct.pagesByURL[""]; ok { ct.Open(root.URL) return ct } ct.Open(ct.pages[0].URL) return ct } // SetContent is a helper function that calls [Content.SetSource] // with the "content" subdirectory of the given filesystem. func (ct *Content) SetContent(content fs.FS) *Content { return ct.SetSource(fsx.Sub(content, "content")) } // Open opens the page with the given URL and updates the display. // If no pages correspond to the URL, it is opened in the default browser. func (ct *Content) Open(url string) *Content { ct.open(url, true) return ct } func (ct *Content) addHistory(pg *bcontent.Page) { ct.historyIndex = len(ct.history) ct.history = append(ct.history, pg) ct.saveWebURL() } // loadPage loads the current page content into the given frame if it is not already loaded. func (ct *Content) loadPage(w *core.Frame) error { if ct.renderedPage == ct.currentPage { return nil } w.DeleteChildren() b, err := ct.currentPage.ReadContent(ct.pagesByCategory) if err != nil { return err } ct.currentPage.ParseSpecials(b) err = htmlcore.ReadMD(ct.Context, w, b) if err != nil { return err } ct.leftFrame.DeleteChildren() ct.makeTableOfContents(w, ct.currentPage) ct.makeCategories() ct.leftFrame.Update() ct.renderedPage = ct.currentPage return nil } // makeTableOfContents makes the table of contents and adds it to [Content.leftFrame] // based on the headings in the given frame. func (ct *Content) makeTableOfContents(w *core.Frame, pg *bcontent.Page) { ct.tocNodes = map[string]*core.Tree{} contents := core.NewTree(ct.leftFrame).SetText("<b>Contents</b>") contents.OnSelect(func(e events.Event) { if contents.IsRootSelected() { ct.rightFrame.ScrollDimToContentStart(math32.Y) ct.currentHeading = "" ct.saveWebURL() } }) // last is the most recent tree node for each heading level, used for nesting. last := map[int]*core.Tree{} w.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool { tx, ok := cw.(*core.Text) if !ok { return tree.Continue } tag := tx.Property("tag") switch tag { case "h1", "h2", "h3", "h4", "h5", "h6": num := errors.Log1(strconv.Atoi(tag.(string)[1:])) parent := contents // Our parent is the last heading with a lower level (closer to h1). for i := num - 1; i >= 1; i-- { if last[i] != nil { parent = last[i] break } } tr := core.NewTree(parent).SetText(tx.Text) last[num] = tr kebab := strcase.ToKebab(tr.Text) ct.tocNodes[kebab] = tr tr.OnSelect(func(e events.Event) { tx.ScrollThisToTop() ct.currentHeading = kebab ct.saveWebURL() }) } return tree.Continue }) if contents.NumChildren() == 0 { contents.Delete() } } // makeCategories makes the categories tree for the current page and adds it to [Content.leftFrame]. func (ct *Content) makeCategories() { if len(ct.categories) == 0 { return } cats := core.NewTree(ct.leftFrame).SetText("<b>Categories</b>") cats.OnSelect(func(e events.Event) { if cats.IsRootSelected() { ct.Open("") } }) for _, cat := range ct.categories { catTree := core.NewTree(cats).SetText(cat).SetClosed(true) if ct.currentPage.Name == cat { catTree.SetSelected(true) } catTree.OnSelect(func(e events.Event) { if catPage := ct.pageByName(cat); catPage != nil { ct.Open(catPage.URL) } }) for _, pg := range ct.pagesByCategory[cat] { pgTree := core.NewTree(catTree).SetText(pg.Name) if pg == ct.currentPage { pgTree.SetSelected(true) catTree.SetClosed(false) } pgTree.OnSelect(func(e events.Event) { ct.Open(pg.URL) }) } } } // embedPage handles an <embed-page> element by embedding the lead section // (content before the first heading) into the current page, with a heading // and a *Main page: [[Name]]* link added at the start as well. The name of // the embedded page is the case-insensitive src attribute of the current // html element. A title attribute may also be specified to override the // heading text. func (ct *Content) embedPage(ctx *htmlcore.Context) error { src := htmlcore.GetAttr(ctx.Node, "src") if src == "" { return fmt.Errorf("missing src attribute in <embed-page>") } pg := ct.pageByName(src) if pg == nil { return fmt.Errorf("page %q not found in <embed-page>", src) } title := htmlcore.GetAttr(ctx.Node, "title") if title == "" { title = pg.Name } b, err := pg.ReadContent(ct.pagesByCategory) if err != nil { return err } lead, _, _ := bytes.Cut(b, []byte("\n#")) heading := fmt.Sprintf("## %s\n\n*Main page: [[%s]]*\n\n", title, pg.Name) res := append([]byte(heading), lead...) return htmlcore.ReadMD(ctx, ctx.BlockParent, res) } // setStageTitle sets the title of the stage based on the current page URL. func (ct *Content) setStageTitle() { if rw := ct.Scene.RenderWindow(); rw != nil && ct.currentPage != nil { name := ct.currentPage.Name if ct.currentPage.URL == "" { // Root page just gets app name name = core.TheApp.Name() } rw.SetStageTitle(name) } } // 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/content" "cogentcore.org/core/core" "cogentcore.org/core/htmlcore" _ "cogentcore.org/core/yaegicore" ) //go:embed content var econtent embed.FS func main() { b := core.NewBody("Cogent Content Example") ct := content.NewContent(b).SetContent(econtent) ct.Context.AddWikilinkHandler(htmlcore.GoDocWikilink("doc", "cogentcore.org/core")) b.AddTopBar(func(bar *core.Frame) { core.NewToolbar(bar).Maker(ct.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 content import ( "fmt" "strings" "cogentcore.org/core/base/errors" "cogentcore.org/core/base/labels" "cogentcore.org/core/base/strcase" "cogentcore.org/core/content/bcontent" "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/math32" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/styles/states" "cogentcore.org/core/text/csl" "cogentcore.org/core/tree" ) // citeWikilink processes citation links, which start with @ func (ct *Content) citeWikilink(text string) (url string, label string) { if len(text) == 0 || text[0] != '@' { // @CiteKey reference citations return "", "" } ref := text[1:] cs := csl.Parenthetical if len(ref) > 1 && ref[0] == '^' { cs = csl.Narrative ref = ref[1:] } url = "ref://" + ref if ct.References == nil { return url, ref } it, has := ct.References.AtTry(ref) if has { return url, csl.CiteDefault(cs, it) } return url, ref } // mainWikilink processes all other wikilinks. // page -> Page, page // page|label -> Page, label // page#heading -> Page#heading, heading // #heading -> ThisPage#heading, heading // Page is the resolved page name. // heading can be a special id, or id:element to find elements within a special, // e.g., #sim_neuron:Run Cycles func (ct *Content) mainWikilink(text string) (url string, label string) { name, label, _ := strings.Cut(text, "|") name, heading, _ := strings.Cut(name, "#") if name == "" { // A link with a blank page links to the current page name = ct.currentPage.Name } else if heading == "" { if pg := ct.pageByName(name); pg == ct.currentPage { // if just a link to current page, don't render link // this can happen for embedded pages that refer to embedder return "", "" } } pg := ct.pageByName(name) if pg == nil { return "", "" } if label == "" { if heading != "" { label = ct.wikilinkLabel(pg, heading) } else { label = name } } if heading != "" { return pg.URL + "#" + heading, label } return pg.URL, label } // wikilinkLabel returns a label for given heading, for given page. func (ct *Content) wikilinkLabel(pg *bcontent.Page, heading string) string { label := heading sl := pg.SpecialLabel(heading) if sl != "" { label = sl } else { colon := strings.Index(heading, ":") if colon > 0 { sl = pg.SpecialLabel(heading[:colon]) if sl != "" { label = sl + ":" + heading[colon+1:] } } } return label } // open opens the page with the given URL and updates the display. // It optionally adds the page to the history. func (ct *Content) open(url string, history bool) { if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") { core.TheApp.OpenURL(url) return } if strings.HasPrefix(url, "ref://") { ct.openRef(url) return } url = strings.ReplaceAll(url, "/#", "#") url, heading, _ := strings.Cut(url, "#") pg := ct.pagesByURL[url] if pg == nil { // We want only the URL after the last slash for automatic redirects // (old URLs could have nesting). last := url if li := strings.LastIndex(url, "/"); li >= 0 { last = url[li+1:] } pg = ct.similarPage(last) if pg == nil { core.ErrorSnackbar(ct, errors.New("no pages available")) } else { core.MessageSnackbar(ct, fmt.Sprintf("Redirected from %s", url)) } } heading = bcontent.SpecialToKebab(heading) ct.currentHeading = heading if ct.currentPage == pg { ct.openHeading(heading) return } ct.currentPage = pg if history { ct.addHistory(pg) } ct.Scene.Update() // need to update the whole scene to also update the toolbar // We can only scroll to the heading after the page layout has been updated, so we defer. ct.Defer(func() { ct.setStageTitle() ct.openHeading(heading) }) } // openRef opens a ref:// reference url. func (ct *Content) openRef(url string) { pg := ct.pagesByURL["references"] if pg == nil { core.MessageSnackbar(ct, "references page not generated, use mdcite in csl package") return } ref := strings.TrimPrefix(url, "ref://") ct.currentPage = pg ct.addHistory(pg) ct.Scene.Update() ct.Defer(func() { ct.setStageTitle() ct.openID(ref, "") }) } func (ct *Content) openHeading(heading string) { if heading == "" { ct.rightFrame.ScrollDimToContentStart(math32.Y) return } idname := "" // in case of #id:element element := "" colon := strings.Index(heading, ":") if colon > 0 { idname = heading[:colon] element = heading[colon+1:] } tr := ct.tocNodes[strcase.ToKebab(heading)] if tr == nil { found := false if idname != "" && element != "" { found = ct.openID(idname, element) if !found { found = ct.openID(heading, "") } } else { found = ct.openID(heading, "") } if !found { errors.Log(fmt.Errorf("heading %q not found", heading)) } return } tr.SelectEvent(events.SelectOne) } func (ct *Content) openID(id, element string) bool { if id == "" { ct.rightFrame.ScrollDimToContentStart(math32.Y) return true } var found *core.WidgetBase ct.rightFrame.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool { // if found != nil { // return tree.Break // } if cwb.Name != id { return tree.Continue } found = cwb return tree.Break }) if found == nil { return false } if element != "" { el := ct.elementInSpecial(found, element) if el != nil { found = el } } found.SetFocus() found.SetState(true, states.Active) found.Style() found.NeedsRender() return true } // elementInSpecial looks for given element within a special item. func (ct *Content) elementInSpecial(sp *core.WidgetBase, element string) *core.WidgetBase { pathPrefix := "" hasPath := false if strings.Contains(element, "/") { pathPrefix, element, hasPath = strings.Cut(element, "/") } if cl, ok := sp.Parent.(*core.Collapser); ok { // for code nxt := tree.NextSibling(cl) if nxt != nil { sp = nxt.(core.Widget).AsWidget() } else { sp = cl.Parent.(core.Widget).AsWidget() // todo: not sure when this is good } } var found *core.WidgetBase sp.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool { if found != nil { return tree.Break } if !cwb.IsDisplayable() { return tree.Continue } if hasPath && !strings.Contains(cw.AsTree().Path(), pathPrefix) { return tree.Continue } label := labels.ToLabel(cw) if !strings.EqualFold(label, element) { return tree.Continue } if cwb.AbilityIs(abilities.Focusable) { found = cwb return tree.Break } next := core.AsWidget(tree.Next(cwb)) if next.AbilityIs(abilities.Focusable) { found = next return tree.Break } return tree.Continue }) return found } // 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 content import ( "cogentcore.org/core/content/bcontent" "github.com/adrg/strutil/metrics" ) // similarPage returns the page most similar to the given URL, used for automatic 404 redirects. func (ct *Content) similarPage(url string) *bcontent.Page { m := metrics.NewJaccard() m.CaseSensitive = false var best *bcontent.Page bestSimilarity := -1.0 for _, page := range ct.pages { similarity := m.Compare(url, page.URL) if similarity > bestSimilarity { best = page bestSimilarity = similarity } } return best } // Code generated by "core generate"; DO NOT EDIT. package content import ( "cogentcore.org/core/tree" "cogentcore.org/core/types" ) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/content.Content", IDName: "content", Doc: "Content manages and displays the content of a set of pages.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Source", Doc: "Source is the source filesystem for the content.\nIt should be set using [Content.SetSource] or [Content.SetContent]."}, {Name: "pages", Doc: "pages are the pages that constitute the content."}}}) // NewContent returns a new [Content] with the given optional parent: // Content manages and displays the content of a set of pages. func NewContent(parent ...tree.Node) *Content { return tree.New[Content](parent...) } // 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:build !js package content func (ct *Content) getWebURL() string { return "" } func (ct *Content) saveWebURL() {} func (ct *Content) handleWebPopState() {} // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package core import ( "slices" "time" ) // Animation represents the data for a widget animation. // You can call [WidgetBase.Animate] to create a widget animation. // Animations are stored on the [Scene]. type Animation struct { // Func is the animation function, which is run every time the [Scene] // receives a paint tick, which is usually at the same rate as the refresh // rate of the monitor. It receives the [Animation] object so that // it can references things such as [Animation.Dt] and set things such as // [Animation.Done]. Func func(a *Animation) // Widget is the widget associated with the animation. The animation will // pause if the widget is not visible, and it will end if the widget is destroyed. Widget *WidgetBase // Dt is the amount of time in milliseconds that has passed since the // last animation frame/step/tick. Dt float32 // Done can be set to true to permanently stop the animation; the [Animation] object // will be removed from the [Scene] at the next frame. Done bool // lastTime is the last time this animation was run. lastTime time.Time } // Animate adds a new [Animation] to the [Scene] for the widget. The given function is run // at every tick, and it receives the [Animation] object so that it can reference and modify // things on it; see the [Animation] docs for more information on things such as [Animation.Dt] // and [Animation.Done]. func (wb *WidgetBase) Animate(f func(a *Animation)) { a := &Animation{ Func: f, Widget: wb, } wb.Scene.Animations = append(wb.Scene.Animations, a) } // runAnimations runs the [Scene.Animations]. func (sc *Scene) runAnimations() { if len(sc.Animations) == 0 { return } for _, a := range sc.Animations { if a.Widget == nil || a.Widget.This == nil { a.Done = true } if a.Done || !a.Widget.IsVisible() { continue } if a.lastTime.IsZero() { a.Dt = 16.66666667 // 60 FPS fallback } else { a.Dt = float32(time.Since(a.lastTime).Seconds()) * 1000 } a.Func(a) a.lastTime = time.Now() } sc.Animations = slices.DeleteFunc(sc.Animations, func(a *Animation) bool { return a.Done }) } // 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/math32" "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(math32.Vec2(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 } res[0] = sv.RenderImage() sv.SetSize(math32.Vec2(32, 32)) res[1] = sv.RenderImage() sv.SetSize(math32.Vec2(48, 48)) res[2] = sv.RenderImage() 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" ) // 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)) 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/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.SendClickOnEnter() 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() { 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() { 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/paint/ppath" "cogentcore.org/core/styles" "cogentcore.org/core/styles/units" ) // 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.Painter) // painter is the paint painter used for drawing. painter *paint.Painter } 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 c.painter = &c.Scene.Painter sty := styles.NewPaint() sty.Transform = math32.Translate2D(c.Geom.Pos.Content.X, c.Geom.Pos.Content.Y).Scale(sz.X, sz.Y) c.painter.PushContext(sty, nil) c.painter.VectorEffect = ppath.VectorEffectNonScalingStroke c.Draw(c.painter) c.painter.PopContext() } // 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/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/styles/states" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/parse/complete" "cogentcore.org/core/text/text" "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 = text.Center s.Border.Radius = styles.BorderRadiusSmall s.Padding.Set(units.Dp(8), units.Dp(16)) s.CenterAll() // 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() } 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) } } } }) 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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package core import ( "cogentcore.org/core/colors" "cogentcore.org/core/events" "cogentcore.org/core/icons" "cogentcore.org/core/styles" "cogentcore.org/core/styles/states" ) // Collapser is a widget that can be collapsed or expanded by a user. // The [Collapser.Summary] is always visible, and the [Collapser.Details] // are only visible when the [Collapser] is expanded with [Collapser.Open] // equal to true. // // You can directly add any widgets to the [Collapser.Summary] and [Collapser.Details] // by specifying one of them as the parent in calls to New{WidgetName}. // Collapser is similar to HTML's <details> and <summary> tags. type Collapser struct { Frame // Open is whether the collapser is currently expanded. It defaults to false. Open bool // Summary is the part of the collapser that is always visible. Summary *Frame `set:"-"` // Details is the part of the collapser that is only visible when // the collapser is expanded. Details *Frame `set:"-"` } func (cl *Collapser) Init() { cl.Frame.Init() cl.Styler(func(s *styles.Style) { s.Direction = styles.Column s.Grow.Set(1, 0) }) } func (cl *Collapser) OnAdd() { cl.Frame.OnAdd() cl.Summary = NewFrame(cl) cl.Summary.Styler(func(s *styles.Style) { s.Grow.Set(1, 0) s.Align.Content = styles.Center s.Align.Items = styles.Center s.Gap.X.Em(0.1) }) toggle := NewSwitch(cl.Summary).SetType(SwitchCheckbox).SetIconOn(icons.KeyboardArrowDown).SetIconOff(icons.KeyboardArrowRight) toggle.SetName("toggle") Bind(&cl.Open, toggle) toggle.Styler(func(s *styles.Style) { s.Color = colors.Scheme.Primary.Base s.Padding.Zero() }) toggle.OnChange(func(e events.Event) { cl.Update() }) cl.Details = NewFrame(cl) cl.Details.Styler(func(s *styles.Style) { s.Grow.Set(1, 0) s.Direction = styles.Column }) cl.Details.Updater(func() { cl.Details.SetState(!cl.Open, states.Invisible) }) } // 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/text/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 _renderWindowFlagsValues = []renderWindowFlags{0, 1, 2, 3, 4, 5} // renderWindowFlagsN is the highest valid value for type renderWindowFlags, plus one. const renderWindowFlagsN renderWindowFlags = 6 var _renderWindowFlagsValueMap = map[string]renderWindowFlags{`IsRendering`: 0, `RenderSkipped`: 1, `Resize`: 2, `StopEventLoop`: 3, `Closing`: 4, `GotFocus`: 5} var _renderWindowFlagsDescMap = map[renderWindowFlags]string{0: `winIsRendering indicates that the renderAsync function is running.`, 1: `winRenderSkipped indicates that a render update was skipped, so another update will be run to ensure full updating.`, 2: `winResize indicates that the window was just resized.`, 3: `winStopEventLoop indicates that the event loop should be stopped.`, 4: `winClosing is whether the window is closing.`, 5: `winGotFocus indicates that have we received focus.`} var _renderWindowFlagsMap = map[renderWindowFlags]string{0: `IsRendering`, 1: `RenderSkipped`, 2: `Resize`, 3: `StopEventLoop`, 4: `Closing`, 5: `GotFocus`} // String returns the string representation of this renderWindowFlags value. func (i renderWindowFlags) String() string { return enums.BitFlagString(i, _renderWindowFlagsValues) } // BitIndexString returns the string representation of this renderWindowFlags value // if it is a bit index value (typically an enum constant), and // not an actual bit flag value. func (i renderWindowFlags) BitIndexString() string { return enums.String(i, _renderWindowFlagsMap) } // SetString sets the renderWindowFlags value from its string representation, // and returns an error if the string is invalid. func (i *renderWindowFlags) SetString(s string) error { *i = 0; return i.SetStringOr(s) } // SetStringOr sets the renderWindowFlags value from its string representation // while preserving any bit flags already set, and returns an // error if the string is invalid. func (i *renderWindowFlags) SetStringOr(s string) error { return enums.SetStringOr(i, s, _renderWindowFlagsValueMap, "renderWindowFlags") } // Int64 returns the renderWindowFlags value as an int64. func (i renderWindowFlags) Int64() int64 { return int64(i) } // SetInt64 sets the renderWindowFlags value from an int64. func (i *renderWindowFlags) SetInt64(in int64) { *i = renderWindowFlags(in) } // Desc returns the description of the renderWindowFlags value. func (i renderWindowFlags) Desc() string { return enums.Desc(i, _renderWindowFlagsDescMap) } // renderWindowFlagsValues returns all possible values for the type renderWindowFlags. func renderWindowFlagsValues() []renderWindowFlags { return _renderWindowFlagsValues } // Values returns all possible values for the type renderWindowFlags. func (i renderWindowFlags) Values() []enums.Enum { return enums.Values(_renderWindowFlagsValues) } // HasFlag returns whether these bit flags have the given bit flag set. func (i *renderWindowFlags) 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 *renderWindowFlags) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i renderWindowFlags) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *renderWindowFlags) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "renderWindowFlags") } 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} // widgetFlagsN is the highest valid value for type widgetFlags, plus one. const widgetFlagsN widgetFlags = 2 var _widgetFlagsValueMap = map[string]widgetFlags{`ValueNewWindow`: 0, `NeedsRender`: 1} 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.`} var _widgetFlagsMap = map[widgetFlags]string{0: `ValueNewWindow`, 1: `NeedsRender`} // 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" "os" "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/paint" "cogentcore.org/core/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/styles/states" "cogentcore.org/core/styles/units" "cogentcore.org/core/system" "cogentcore.org/core/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. focus Widget // currently attended widget. Use SetAttend. attended 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 tree.IsNil(em.focus) && e.Type() != events.KeyDown && e.Type() != events.KeyUp { switch { case !tree.IsNil(em.startFocus): if DebugSettings.FocusTrace { fmt.Println(em.scene, "StartFocus:", em.startFocus) } em.setFocus(em.startFocus) case !tree.IsNil(em.prevFocus): if DebugSettings.FocusTrace { fmt.Println(em.scene, "PrevFocus:", em.prevFocus) } em.setFocus(em.prevFocus) em.prevFocus = nil } } if !tree.IsNil(em.focus) { em.focus.AsTree().WalkUpParent(func(k tree.Node) bool { wb := AsWidget(k) if !wb.IsDisplayable() { 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.IsDisplayable() { 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 !tree.IsNil(em.longHoverWidget) && 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 !tree.IsNil(em.slide) { em.slide.AsWidget().HandleEvent(e) em.slide.AsWidget().Send(events.SlideMove, e) e.SetHandled() return } case events.Scroll: if !tree.IsNil(em.scroll) { scInTime := time.Since(em.lastScrollTime) < DeviceSettings.ScrollFocusTime if scInTime { em.scroll.AsWidget().HandleEvent(e) if e.IsHandled() { em.lastScrollTime = time.Now() } return } 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 } // everyone gets the primary event who is in scope, deepest first if et == events.Scroll { if wb.AbilityIs(abilities.ScrollableUnattended) || (wb.StateIs(states.Focused) || wb.StateIs(states.Attended)) { w.AsWidget().HandleEvent(e) } } else { w.AsWidget().HandleEvent(e) } if tree.IsNil(w) { // died while handling continue } switch et { case events.MouseMove: em.scroll = nil if tree.IsNil(move) && wb.Styles.Abilities.IsHoverable() { move = w } case events.MouseDown: em.scroll = nil // in ScRenderBBoxes, everyone is effectively pressable if tree.IsNil(press) && (wb.Styles.Abilities.IsPressable() || sc.renderBBoxes) { press = w } if tree.IsNil(dragPress) && wb.AbilityIs(abilities.Draggable) { dragPress = w } if tree.IsNil(slidePress) && wb.AbilityIs(abilities.Slideable) { // On mobile, sliding results in scrolling, so we must have the appropriate // scrolling attention to allow sliding. if TheApp.SystemPlatform().IsMobile() && !wb.AbilityIs(abilities.ScrollableUnattended) && !(wb.StateIs(states.Focused) || wb.StateIs(states.Attended)) { continue } 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 tree.IsNil(up) && (wb.Styles.Abilities.IsPressable() || sc.renderBBoxes) { up = w } case events.Scroll: if !wb.AbilityIs(abilities.ScrollableUnattended) && !(wb.StateIs(states.Focused) || wb.StateIs(states.Attended)) { continue } if e.IsHandled() { if tree.IsNil(em.scroll) { em.scroll = w em.lastScrollTime = time.Now() } } } } switch et { case events.MouseDown: if !tree.IsNil(press) { em.press = press em.setAttend(press) } if !tree.IsNil(dragPress) { em.dragPress = dragPress } if !tree.IsNil(slidePress) { em.slidePress = slidePress } if !tree.IsNil(repeatClick) { 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 !tree.IsNil(em.drag) { // this means we missed the drop em.dragHovers = em.updateHovers(hovs, em.dragHovers, e, events.DragEnter, events.DragLeave) em.dragDrop(em.drag, e) break } 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 !tree.IsNil(pselw) { pselw.AsWidget().NeedsRender() } if !tree.IsNil(sc.selectedWidget) { sc.selectedWidget.AsWidget().NeedsRender() } } } em.hovers = em.updateHovers(hovs, em.hovers, e, events.MouseEnter, events.MouseLeave) em.handleLongHover(e) case events.MouseDrag: if !tree.IsNil(em.drag) { 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 !tree.IsNil(em.dragPress) && em.dragStartCheck(e, DeviceSettings.DragStartTime, DeviceSettings.DragStartDistance) { em.cancelRepeatClick() em.cancelLongPress() em.dragPress.AsWidget().Send(events.DragStart, e) e.SetHandled() } else if !tree.IsNil(em.slidePress) && em.dragStartCheck(e, DeviceSettings.SlideStartTime, DeviceSettings.DragStartDistance) { 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 !tree.IsNil(em.longPressWidget) { em.handleLongPress(e) } case events.MouseUp: em.cancelRepeatClick() if !tree.IsNil(em.slide) { em.slide.AsWidget().Send(events.SlideStop, e) e.SetHandled() em.slide = nil em.press = nil } if !tree.IsNil(em.drag) { 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 && !tree.IsNil(up) && !(!tree.IsNil(em.longPressWidget) && 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 && !tree.IsNil(em.press) { 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 !tree.IsNil(up) && TheApp.Platform().IsMobile() { up.AsWidget().Send(events.MouseLeave, e) } case events.Scroll: switch { case !tree.IsNil(em.slide): em.slide.AsWidget().HandleEvent(e) case !tree.IsNil(em.drag): em.drag.AsWidget().HandleEvent(e) case !tree.IsNil(em.press): 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 && !tree.IsNil(prv) { prv.AsWidget().Send(leave, e) } } for _, cur := range hov { wasIn := false for _, prv := range prev { if prv == cur { wasIn = true break } } if !wasIn { 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 tree.IsNil(deep) { // fmt.Println("no deep") if tree.IsNil(*w) { // 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 !tree.IsNil(*w) { (*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 tree.IsNil(*w) { 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] && ly.Scrolls[d] != nil { 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 !tree.IsNil(em.longPressWidget) && 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 tree.IsNil(em.repeatClick) || !em.repeatClick.AsWidget().IsVisible() { return } delay := DeviceSettings.RepeatClickTime if em.repeatClickTimer == nil { delay *= 8 } em.repeatClickTimer = time.AfterFunc(delay, func() { if tree.IsNil(em.repeatClick) || !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) } // DragMenuAddModText adds info about key modifiers for a drag drop menu. 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 em.scene.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool { cwb.dragStateReset() return tree.Continue }) em.scene.Restyle() 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 !tree.IsNil(em.focus) { 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 } 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 !tree.IsNil(w) { 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 tree.IsNil(cfoc) { em.focus = nil cfoc = nil } if !tree.IsNil(cfoc) && !tree.IsNil(w) && cfoc == w { if DebugSettings.FocusTrace { fmt.Println(em.scene, "Already Focus:", cfoc) } return false } if !tree.IsNil(cfoc) { if DebugSettings.FocusTrace { fmt.Println(em.scene, "Losing focus:", cfoc) } cfoc.AsWidget().Send(events.FocusLost) } em.focus = w if sendEvent && !tree.IsNil(w) { 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 tree.IsNil(em.focus) { 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.IsDisplayable() && !wb.StateIs(states.Disabled) && wb.AbilityIs(abilities.Focusable) }) em.setFocus(next) return !tree.IsNil(next) } // 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.IsDisplayable() { 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.IsDisplayable() { 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 tree.IsNil(em.focus) { 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.IsDisplayable() && !wb.StateIs(states.Disabled) && wb.AbilityIs(abilities.Focusable) }) em.setFocus(prev) return !tree.IsNil(prev) } // 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 tree.IsNil(em.startFocus) && !em.startFocusFirst { // fmt.Println("no start focus") return false } sf := em.startFocus em.startFocus = nil if tree.IsNil(sf) { em.focusFirst() } else { // fmt.Println("start focus on:", sf) em.setFocus(sf) } return true } // setAttend sets attended to given item, and returns true if attended changed. // If item is nil, then nothing is attended. // This sends the [events.Attend] event to the widget. func (em *Events) setAttend(w Widget) bool { if DebugSettings.FocusTrace { fmt.Println(em.scene, "SetAttendEvent:", w) } got := em.setAttendImpl(w, true) // sends event if !got { if DebugSettings.FocusTrace { fmt.Println(em.scene, "SetAttendEvent: Failed", w) } return false } return got } // setAttendImpl sets attended to given item, and returns true if attended changed. // If item is nil, then nothing has attended. // sendEvent determines whether the events.Attend event is sent to the focused item. func (em *Events) setAttendImpl(w Widget, sendEvent bool) bool { catd := em.attended if tree.IsNil(catd) { em.attended = nil catd = nil } if catd != nil && !tree.IsNil(w) && catd == w { if DebugSettings.FocusTrace { fmt.Println(em.scene, "Already Attend:", catd) } return false } if catd != nil { if DebugSettings.FocusTrace { fmt.Println(em.scene, "Losing attend:", catd) } catd.AsWidget().Send(events.AttendLost) } em.attended = w if sendEvent && !tree.IsNil(w) { w.AsWidget().Send(events.Attend) } 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: img := sc.renderer.Image() dstr := time.Now().Format(time.DateOnly + "-" + "15-04-05") var sz string if img != nil { sz = fmt.Sprint(img.Bounds().Size()) fnm := filepath.Join(TheApp.AppDataDir(), "screenshot-"+sc.Name+"-"+dstr+".png") if errors.Log(imagex.Save(img, fnm)) == nil { MessageSnackbar(sc, "Saved screenshot to: "+strings.ReplaceAll(fnm, " ", `\ `)+sz) } } else { MessageSnackbar(sc, "Save screenshot: no render image") } sc.RenderWidget() sv := paint.RenderToSVG(&sc.Painter) fnm := filepath.Join(TheApp.AppDataDir(), "screenshot-"+sc.Name+"-"+dstr+".svg") errors.Log(os.WriteFile(fnm, sv, 0666)) MessageSnackbar(sc, "Saved SVG screenshot to: "+strings.ReplaceAll(fnm, " ", `\ `)+sz) 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() 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 tree.IsNil(sa) { 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/base/fsx" "cogentcore.org/core/colors" "cogentcore.org/core/cursors" "cogentcore.org/core/events" "cogentcore.org/core/icons" "cogentcore.org/core/keymap" "cogentcore.org/core/styles" "cogentcore.org/core/system" "cogentcore.org/core/text/parse/complete" "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().RunWindowDialog(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 = fsx.Filename // 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/keylist" "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 // Modified optionally highlights and tracks fields that have been modified // through an OnChange event. If present, it replaces the default value highlighting // and resetting logic. Ignored if nil. Modified map[string]bool // structFields are the fields of the current struct, keys are field paths. structFields keylist.List[string, *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 keylist.List[string, *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:"-"` } path := field.Name + " • " + sfield.Name fields.Add(path, &structField{path: path, field: sfield, value: value, parent: parent}) }) } else { fields.Add(field.Name, &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 := range fm.structFields.Len() { f := fm.structFields.Values[i] fieldPath := fm.structFields.Keys[i] 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", fieldPath) // 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" } // Using the type name ensures that widgets are specific to the type, // even if they happen to have the same name. Using the path to index // the structFields ensures safety against any [ShouldDisplayer] // updates (see #1390). valnm := fmt.Sprintf("value-%s-%s", fieldPath, typnm) 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 || fm.Modified != nil { if hasDef { w.SetTooltip("(Default: " + def + ") " + w.Tooltip) } var isDef bool w.Styler(func(s *styles.Style) { f := fm.structFields.At(fieldPath) dcr := "(Double click to reset to default) " if fm.Modified != nil { isDef = !fm.Modified[f.path] dcr = "(Double click to mark as not modified) " } else { isDef = reflectx.ValueIsDefault(f.value, def) } 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.At(fieldPath) if isDef { return } e.SetHandled() var err error if fm.Modified != nil { fm.Modified[f.path] = false } else { err = reflectx.SetFromDefaultTag(f.value, def) } if err != nil { ErrorSnackbar(w, err, "Error setting default value") } else { w.Update() valueWidget.AsWidget().Update() if fm.Modified == nil { 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.At(fieldPath) fm.Send(events.Input, e) if f.field.Tag.Get("immediate") == "+" { wb.SendChange(e) } }) if !fm.IsReadOnly() && !readOnlyTag { wb.OnChange(func(e events.Event) { if fm.Modified != nil { fm.Modified[f.path] = true } fm.SendChange(e) if hasDef || fm.Modified != nil { labelWidget.Update() } if fm.isShouldDisplayer { fm.Update() } }) } wb.Updater(func() { wb.SetReadOnly(fm.IsReadOnly() || readOnlyTag) f := fm.structFields.At(fieldPath) 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/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/styles/states" "cogentcore.org/core/text/parse/complete" "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 `copier:"-" json:"-" xml:"-" set:"-"` // handleKeyNav indicates whether this frame should handle keyboard // navigation events using the default handlers. Set to false to allow // custom event handling. handleKeyNav bool // 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.handleKeyNav = true fr.Styler(func(s *styles.Style) { s.SetAbilities(true, abilities.ScrollableUnattended) }) 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) if TheApp.SystemPlatform().IsMobile() { s.SetAbilities(true, abilities.Slideable) } } }) fr.OnFinal(events.KeyChord, func(e events.Event) { if !fr.handleKeyNav { return } 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) { if fr.AbilityIs(abilities.ScrollableUnattended) || (fr.StateIs(states.Focused) || fr.StateIs(states.Attended)) { fr.scrollDelta(e) } }) // We treat slide events on frames as scroll events on mobile. prevVels := []math32.Vector2{} fr.On(events.SlideStart, func(e events.Event) { if !TheApp.SystemPlatform().IsMobile() { return } // Stop any existing scroll animations for this frame. for _, anim := range fr.Scene.Animations { if anim.Widget.This == fr.This { anim.Done = true } } }) fr.On(events.SlideMove, func(e events.Event) { if !TheApp.SystemPlatform().IsMobile() { return } // We must negate the delta for "natural" scrolling behavior. del := math32.FromPoint(e.PrevDelta()).Negate() fr.scrollDelta(events.NewScroll(e.WindowPos(), del, e.Modifiers())) time := float32(e.SincePrev().Seconds()) * 1000 vel := del.DivScalar(time) if len(prevVels) >= 3 { prevVels = append(prevVels[1:], vel) } else { prevVels = append(prevVels, vel) } }) fr.On(events.SlideStop, func(e events.Event) { if !TheApp.SystemPlatform().IsMobile() { return } // If we have enough velocity over the last few slide events, // we continue scrolling in an animation while slowly decelerating // for a smoother experience. if len(prevVels) == 0 { return } vel := math32.Vector2{} for _, vi := range prevVels { vel.SetAdd(vi) } vel.SetDivScalar(float32(len(prevVels))) prevVels = prevVels[:0] // reset for next scroll if vel.Length() < 1 { return } i := 0 t := float32(0) fr.Animate(func(a *Animation) { t += a.Dt // See https://medium.com/@esskeetit/scrolling-mechanics-of-uiscrollview-142adee1142c vel.SetMulScalar(math32.Pow(0.998, a.Dt)) // TODO: avoid computing Pow each time? dx := vel.MulScalar(a.Dt) fr.scrollDelta(events.NewScroll(e.WindowPos(), dx, e.Modifiers())) i++ if t > 2000 { a.Done = true } }) }) } 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.StartRender() { fr.This.(Widget).Render() fr.RenderChildren() fr.renderParts() fr.RenderScrolls() fr.EndRender() } } // 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 } 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 " + strings.ToLower(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, abilities.ScrollableUnattended) 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" "reflect" "strconv" "strings" "cogentcore.org/core/base/reflectx" "cogentcore.org/core/styles" "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", "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" case "cogentcore.org/core/textcore.Editor": se.Name.Local = "textarea" } 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)) } if href, ok := wb.Property("href").(string); ok { addAttr(se, "href", href) } 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 } 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: // TODO: just remove the width and height attributes from the source SVGs? icon := strings.ReplaceAll(string(w.Icon), ` width="48" height="48"`, "") b.WriteString(icon) case *SVG: w.SVG.PhysicalWidth = wb.Styles.Min.X w.SVG.PhysicalHeight = wb.Styles.Min.Y err := w.SVG.WriteXML(b, false) if err != nil { return err } } if se.Name.Local == "textarea" && idName == "editor" { b.WriteString(reflectx.Underlying(reflect.ValueOf(w)).FieldByName("Lines").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" "image/color" "strings" "cogentcore.org/core/base/errors" "cogentcore.org/core/colors" "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 // prevColor is the previously rendered color, as uniform. prevColor color.RGBA // prevOpacity is the previously rendered opacity. prevOpacity float32 // image representation of the icon, cached for faster drawing. pixels image.Image } func (ic *Icon) WidgetValue() any { return &ic.Icon } func (ic *Icon) Init() { ic.WidgetBase.Init() ic.Styler(func(s *styles.Style) { s.Min.Set(units.Em(1)) }) } // RerenderSVG forcibly renders the icon, returning the [svg.SVG] // used to render. func (ic *Icon) RerenderSVG() *svg.SVG { ic.pixels = nil ic.prevIcon = "" return ic.renderSVG() } // renderSVG renders the icon if necessary, returning the [svg.SVG] // used to render if it was rendered, otherwise nil. func (ic *Icon) renderSVG() *svg.SVG { sz := ic.Geom.Size.Actual.Content.ToPoint() if sz == (image.Point{}) { return nil } var isz image.Point if ic.pixels != nil { isz = ic.pixels.Bounds().Size() } cc := colors.ToUniform(ic.Styles.Color) if ic.Icon == ic.prevIcon && sz == isz && ic.prevColor == cc && ic.prevOpacity == ic.Styles.Opacity && !ic.NeedsRebuild() { return nil } ic.pixels = nil if !ic.Icon.IsSet() { ic.prevIcon = ic.Icon return nil } sv := svg.NewSVG(ic.Geom.Size.Actual.Content) err := sv.ReadXML(strings.NewReader(string(ic.Icon))) if errors.Log(err) != nil || sv.Root == nil || !sv.Root.HasChildren() { return nil } icons.Used[ic.Icon] = struct{}{} ic.prevIcon = ic.Icon sv.Root.ViewBox.PreserveAspectRatio.SetFromStyle(&ic.Styles) sv.TextShaper = ic.Scene.TextShaper() clr := gradient.ApplyOpacity(ic.Styles.Color, ic.Styles.Opacity) sv.Color = clr sv.Scale = 1 ic.pixels = sv.RenderImage() ic.prevColor = cc ic.prevOpacity = ic.Styles.Opacity return sv } func (ic *Icon) Render() { ic.renderSVG() if ic.pixels == nil { return } r := ic.Geom.ContentBBox sp := ic.Geom.ScrollOffset() ic.Scene.Painter.DrawImage(ic.pixels, r, 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 if r == (image.Rectangle{}) || im.Image.Bounds().Size() == (image.Point{}) { return } 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 = imagex.WrapJS(im.Styles.ResizeImage(im.Image, im.Geom.Size.Actual.Content)) im.prevRenderImage = rimg } im.Scene.Painter.DrawImage(rimg, r, 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 ( "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() } // 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 [Frame] if it implements [Layouter] // or nil otherwise. func AsFrame(n tree.Node) *Frame { if t, ok := n.(Layouter); ok { return t.AsFrame() } return nil } 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, pass) 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 == SizeFinalPass { if grw.X == 0 && !cwb.Styles.GrowWrap { sz.X = cwb.Geom.Size.Alloc.Total.X } if grw.Y == 0 { sz.Y = cwb.Geom.Size.Alloc.Total.Y } } 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 < 0.5 { 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/text/text" "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), 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, _, isValid := lb.rowFromEventPos(e) if !isValid { return } pt := lb.PointToRelPos(e.Pos()) lb.ListGrid.AutoScroll(math32.FromPoint(pt)) 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 = text.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-1) { 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 = lb.lastClick } 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.handleKeyNav = false 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) rht += lg.layout.Gap.Y if err != nil { // fmt.Println("ListGrid Sizing Error:", err) lg.rowHeight = 42 } lg.rowHeight = rht 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 if allocHt > lg.rowHeight { lg.visibleRows = int(math32.Ceil(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) list() *ListBase { ls := tree.ParentByType[Lister](lg) return ls.AsListBase() } func (lg *ListGrid) ScrollChanged(d math32.Dims, sb *Slider) { if d == math32.X { lg.Frame.ScrollChanged(d, sb) return } ls := lg.list() rht := lg.rowHeight quo := sb.Value / rht floor := math32.Floor(quo) ls.StartIndex = int(floor) lg.Geom.Scroll.Y = (floor - quo) * rht ls.ApplyScenePos() ls.UpdateTree() ls.NeedsRender() // ls.NeedsLayout() // needed to recompute size after resize } func (lg *ListGrid) ScrollValues(d math32.Dims) (maxSize, visSize, visPct float32) { if d == math32.X { return lg.Frame.ScrollValues(d) } ls := lg.list() maxSize = float32(max(ls.SliceSize, 1)) * lg.rowHeight visSize = lg.Geom.Size.Alloc.Content.Y 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) * lg.rowHeight) } 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 { ls := lg.list() lg.updateBackgrounds() row, _ := ls.widgetIndex(child) si := row + ls.StartIndex return lg.rowBackground(ls.indexIsSelected(si), si%2 == 1, row == ls.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.Painter rows := lg.layout.Shape.Y cols := lg.layout.Shape.X st := pos offset := 0 ls := lg.list() startIndex := 0 if ls != nil { startIndex = ls.StartIndex offset = startIndex % 2 } for r := 0; r < rows; r++ { si := r + startIndex ht := lg.rowHeight 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(ls.indexIsSelected(si), stripe, r == ls.hoverRow) pc.BlitBox(st, ssz, sbg) st.Y += ht } } // 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{} st.Y = lg.Geom.Scroll.Y got := false for r := 0; r < rows; r++ { ht := lg.rowHeight miny := st.Y if r > 0 { for c := 0; c < cols; c++ { kwt := lg.Child(r*cols + c) if kwt == nil { continue } kw := kwt.(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 // 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().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. if TheApp.Platform() != system.Offscreen { 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.SceneGeom.Pos = sc.SceneGeom.Pos.Sub(sz.Div(2)) // center dialogs by default 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(title); 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 if wb == nil || wb.This == nil { return } 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 in [Scene.MenuSearchDialog]. 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) { sc.MenuSearchDialog("Menu search", msdesc) }) 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() }) }) } // MenuSearchDialog runs the menu search dialog for the scene with // the given title and description text. It includes scenes, toolbar buttons, // and [MenuSearcher]s. func (sc *Scene) MenuSearchDialog(title, text string) { d := NewBody(title) NewText(d).SetType(TextSupporting).SetText(text) 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) } // 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/styles" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/htmltext" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/text" ) // 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 s.SetTextWrap(false) }) 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 = 40.0 / 32 s.Text.Align = text.Center s.Text.AlignV = text.Center case MeterSemicircle: s.Min.Set(units.Dp(112), units.Dp(64)) m.Width.Dp(16) s.Font.Size.Dp(22) s.Text.LineHeight = 28.0 / 22 s.Text.Align = text.Center s.Text.AlignV = text.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.Painter 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.Fill.Color = m.ValueColor m.RenderBoxGeom(m.Geom.Pos.Content, size, st.Border) } return } pc.Stroke.Width = m.Width sw := m.Width.Dots pos := m.Geom.Pos.Content.AddScalar(sw / 2) size := m.Geom.Size.Actual.Content.SubScalar(sw) pc.Fill.Color = colors.Scheme.Surface var txt *shaped.Lines var toff math32.Vector2 if m.Text != "" { sty, tsty := m.Styles.NewRichText() tx, _ := htmltext.HTMLToRich([]byte(m.Text), sty, nil) txt = m.Scene.TextShaper().WrapLines(tx, sty, tsty, &AppearanceSettings.Text, size) toff = txt.Bounds.Size().DivScalar(2) } if m.Type == MeterCircle { r := size.DivScalar(2) c := pos.Add(r) pc.EllipticalArc(c.X, c.Y, r.X, r.Y, 0, 0, 2*math32.Pi) pc.Stroke.Color = st.Background pc.Draw() if m.ValueColor != nil { pc.EllipticalArc(c.X, c.Y, r.X, r.Y, 0, -math32.Pi/2, prop*2*math32.Pi-math32.Pi/2) pc.Stroke.Color = m.ValueColor pc.Draw() } if txt != nil { pc.DrawText(txt, c.Sub(toff)) } return } r := size.Mul(math32.Vec2(0.5, 1)) c := pos.Add(r) pc.EllipticalArc(c.X, c.Y, r.X, r.Y, 0, math32.Pi, 2*math32.Pi) pc.Stroke.Color = st.Background pc.Draw() if m.ValueColor != nil { pc.EllipticalArc(c.X, c.Y, r.X, r.Y, 0, math32.Pi, (1+prop)*math32.Pi) pc.Stroke.Color = m.ValueColor pc.Draw() } if txt != nil { pc.DrawText(txt, 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. // In comparison, [Pages.SetPage] does not update the display and should typically // only be called at the start. func (pg *Pages) Open(name string) *Pages { pg.SetPage(name) pg.Update() return pg } // 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 := sc.Styles.Font.FontHeight() if fontHt == 0 { fontHt = 16 } switch st.Type { case MenuStage: sz.X += scrollWd * 2 maxht := int(float32(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" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" ) // 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, txt, 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" txt := "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, txt, body) return } b := NewBody(title) NewText(b).SetText(title).SetType(TextHeadlineSmall) NewText(b).SetType(TextSupporting).SetText(txt) 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.Font.Family = rich.Monospace s.Text.WhiteSpace = text.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/paint/render" _ "cogentcore.org/core/paint/renderers" // installs default renderer "cogentcore.org/core/styles" "cogentcore.org/core/system" "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. // // If the widget has been deleted, or if the [Scene] has been shown but the render // context is not available, then this will block forever. Enable // [DebugSettingsData.UpdateTrace] in [DebugSettings] to see when that happens. // If the scene has not been shown yet and the render context is nil, it will wait // until the scene is shown before trying again. func (wb *WidgetBase) AsyncLock() { rc := wb.Scene.renderContext() if rc == nil { if wb.Scene.hasFlag(sceneHasShown) { // If the scene has been shown but there is no render context, // we are probably being deleted, so we just block forever. if DebugSettings.UpdateTrace { fmt.Println("AsyncLock: scene shown but no render context; blocking forever:", wb) } select {} } // Otherwise, if we haven't been shown yet, we just wait until we are // and then try again. if DebugSettings.UpdateTrace { fmt.Println("AsyncLock: waiting for scene to be shown:", wb) } onShow := make(chan struct{}) wb.OnShow(func(e events.Event) { onShow <- struct{}{} }) <-onShow wb.AsyncLock() // try again return } rc.Lock() if wb.This == nil { rc.Unlock() if DebugSettings.UpdateTrace { fmt.Println("AsyncLock: widget deleted; blocking forever:", wb) } 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() } func (sc *Scene) Render() { if TheApp.Platform() == system.Web { sc.Painter.Fill.Color = colors.Uniform(colors.Transparent) sc.Painter.Clear() } sc.RenderStandardBox() } // 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) }() sc.runAnimations() 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 } 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() if sc.Painter.Paint != nil { sc.Painter.Paint.UnitContext = sc.Styles.UnitContext } 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 // StartRender starts the rendering process in the Painter, if the // widget is visible, otherwise it returns false. // It pushes our context and bounds onto the render stack. // This must be called as the first step in [Widget.RenderWidget] implementations. func (wb *WidgetBase) StartRender() bool { if wb == nil || wb.This == nil { return false } wb.setFlag(false, widgetNeedsRender) // done! if !wb.IsVisible() { return false } wb.Styles.ComputeActualBackground(wb.parentActualBackground()) pc := &wb.Scene.Painter if pc.State == nil { return false } pc.PushContext(nil, render.NewBoundsRect(wb.Geom.TotalBBox, wb.Styles.Border.Radius.Dots())) pc.Paint.Defaults() // start with default style values if DebugSettings.RenderTrace { fmt.Printf("Render: %v at %v\n", wb.Path(), wb.Geom.TotalBBox) } return true } // EndRender is the last step in [Widget.RenderWidget] implementations after // rendering children. It pops our state off of the render stack. func (wb *WidgetBase) EndRender() { if wb == nil || wb.This == nil { return } pc := &wb.Scene.Painter 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.Stroke.Width pcsc := pc.Stroke.Color pcfc := pc.Fill.Color pcop := pc.Fill.Opacity pc.Stroke.Width.Dot(1) pc.Stroke.Color = colors.Uniform(hct.New(wb.Scene.renderBBoxHue, 100, 50)) pc.Fill.Color = nil if isSelw { fc := pc.Stroke.Color pc.Fill.Color = fc pc.Fill.Opacity = 0.2 } pc.Rectangle(pos.X, pos.Y, sz.X, sz.Y) pc.Draw() // restore pc.Fill.Opacity = pcop pc.Fill.Color = pcfc pc.Stroke.Width = pcsw pc.Stroke.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.PopContext() } // 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.StartRender() { wb.This.(Widget).Render() wb.renderChildren() wb.renderParts() wb.EndRender() } } 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() }) } // 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 }) wb.Events().activateStartFocus() } //////// 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.Painter.Border(pos.X, pos.Y, sz.X, sz.Y, bs) } // RenderStandardBox renders the standard box model, using Actual size. func (wb *WidgetBase) RenderStandardBox() { pos := wb.Geom.Pos.Total sz := wb.Geom.Size.Actual.Total wb.Scene.Painter.StandardBox(&wb.Styles, pos, sz, wb.parentActualBackground()) } // RenderAllocBox renders the standard box model using Alloc size, instead of Actual. func (wb *WidgetBase) RenderAllocBox() { pos := wb.Geom.Pos.Total sz := wb.Geom.Size.Alloc.Total wb.Scene.Painter.StandardBox(&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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build !js package core import ( "image" "cogentcore.org/core/colors" "cogentcore.org/core/math32" "cogentcore.org/core/paint/renderers/rasterx" "cogentcore.org/core/system/composer" "golang.org/x/image/draw" ) func (ps *paintSource) Draw(c composer.Composer) { cd := c.(*composer.ComposerDrawer) rd := ps.renderer.(*rasterx.Renderer) unchanged := len(ps.render) == 0 if !unchanged { rd.Render(ps.render) } img := rd.Image() cd.Drawer.Copy(ps.drawPos, img, img.Bounds(), ps.drawOp, unchanged) } func (ss *scrimSource) Draw(c composer.Composer) { cd := c.(*composer.ComposerDrawer) clr := colors.Uniform(colors.ApplyOpacity(colors.ToUniform(colors.Scheme.Scrim), 0.5)) cd.Drawer.Copy(image.Point{}, clr, ss.bbox, draw.Over, composer.Unchanged) } func (ss *spritesSource) Draw(c composer.Composer) { cd := c.(*composer.ComposerDrawer) for _, sr := range ss.sprites { if !sr.active { continue } cd.Drawer.Copy(sr.drawPos, sr.pixels, sr.pixels.Bounds(), draw.Over, composer.Unchanged) } } //////// fillInsets // fillInsetsSource is a [composer.Source] implementation for fillInsets. type fillInsetsSource struct { rbb, wbb image.Rectangle } func (ss *fillInsetsSource) Draw(c composer.Composer) { cd := c.(*composer.ComposerDrawer) clr := colors.Scheme.Background fill := func(x0, y0, x1, y1 int) { r := image.Rect(x0, y0, x1, y1) if r.Dx() == 0 || r.Dy() == 0 { return } cd.Drawer.Copy(image.Point{}, clr, r, draw.Src, composer.Unchanged) } rb := ss.rbb wb := ss.wbb 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 } // fillInsets fills the window insets, if any, with [colors.Scheme.Background]. func (w *renderWindow) fillInsets(cp composer.Composer) { // 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 } cp.Add(&fillInsetsSource{rbb: rg.Bounds(), wbb: wg.Bounds()}, w) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package core import ( "image" "cogentcore.org/core/paint/render" "cogentcore.org/core/system/composer" "golang.org/x/image/draw" ) //////// Scene // SceneSource returns a [composer.Source] for the given scene // using the given suggested draw operation. func SceneSource(sc *Scene, op draw.Op) composer.Source { if sc.Painter.State == nil || sc.renderer == nil { return nil } render := sc.Painter.RenderDone() return &paintSource{render: render, renderer: sc.renderer, drawOp: op, drawPos: sc.SceneGeom.Pos} } // paintSource is the [composer.Source] for [paint.Painter] content, such as for a [Scene]. type paintSource struct { // render is the render content. render render.Render // renderer is the renderer for drawing the painter content. renderer render.Renderer // drawOp is the [draw.Op] operation: [draw.Src] to copy source, // [draw.Over] to alpha blend. drawOp draw.Op // drawPos is the position offset for the [Image] renderer to // use in its Draw to a [composer.Drawer] (i.e., the [Scene] position). drawPos image.Point } //////// Scrim // ScrimSource returns a [composer.Source] for a scrim with the given bounding box. func ScrimSource(bbox image.Rectangle) composer.Source { return &scrimSource{bbox: bbox} } // scrimSource is a [composer.Source] implementation for a scrim. type scrimSource struct { bbox image.Rectangle } //////// Sprites // SpritesSource returns a [composer.Source] for rendering [Sprites]. func SpritesSource(sprites *Sprites, scpos image.Point) composer.Source { sprites.Lock() defer sprites.Unlock() ss := &spritesSource{} ss.sprites = make([]spriteRender, len(sprites.Order)) for i, kv := range sprites.Order { sp := kv.Value // note: may need to copy pixels but hoping not.. sr := spriteRender{drawPos: sp.Geom.Pos.Add(scpos), pixels: sp.Pixels, active: sp.Active} ss.sprites[i] = sr } sprites.modified = false return ss } // spritesSource is a [composer.Source] implementation for [Sprites]. type spritesSource struct { sprites []spriteRender } // spriteRender holds info sufficient for rendering a sprite. type spriteRender struct { drawPos image.Point pixels *image.RGBA active bool } // 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" "time" "cogentcore.org/core/base/errors" "cogentcore.org/core/colors/matcolor" "cogentcore.org/core/events" "cogentcore.org/core/icons" "cogentcore.org/core/math32" "cogentcore.org/core/system" "cogentcore.org/core/system/composer" "cogentcore.org/core/text/shaped" "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. // // For offscreen testing, Wait is typically never called, as it is // not necessary (the app will already terminate once all tests are done, // and nothing needs to run on the main thread). 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{} // flags are atomic renderWindow flags. flags renderWindowFlags // lastResize is the time stamp of last resize event -- used for efficient updating. lastResize time.Time } // 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.flags.SetFlag(true, winClosing) // 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.flags.SetFlag(false, winClosing) 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 == "" { title = w.title } else 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 Scene sizes after a window has been resized. // It is called on any geometry update, including move and // DPI changes, so it detects what actually needs to be updated. func (w *renderWindow) resized() { rc := w.renderContext() if !w.isVisible() { rc.visible = false return } w.SystemWindow.Lock() rg := w.SystemWindow.RenderGeom() w.SystemWindow.Unlock() curRg := rc.geom curDPI := w.logicalDPI() if curRg == rg { newDPI := false if rc.logicalDPI != curDPI { rc.logicalDPI = curDPI newDPI = true } if DebugSettings.WindowEventTrace { fmt.Printf("Win: %v same-size resized: %v newDPI: %v\n", w.name, curRg, newDPI) } if w.mains.resize(rg) || newDPI { for _, kv := range w.mains.stack.Order { st := kv.Value sc := st.Scene sc.applyStyleScene() } } return } rc.logicalDPI = curDPI 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 w.flags.SetFlag(true, winResize) 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.flags.HasFlag(winClosing) || !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.flags.HasFlag(winStopEventLoop) { w.flags.SetFlag(false, winStopEventLoop) break } e := d.NextEvent() if w.flags.HasFlag(winStopEventLoop) { w.flags.SetFlag(false, winStopEventLoop) 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 { log.Println("Window got event", e) } if et >= events.Window && et <= events.WindowPaint { w.handleWindowEvents(e) rc.Unlock() return } if DebugSettings.EventTrace && (!w.isVisible() || w.SystemWindow.Is(system.Minimized)) { log.Println("got event while invisible:", e) log.Println("w.isClosed:", w.isClosed(), "winClosing flag:", w.flags.HasFlag(winClosing), "syswin !isvis:", !w.SystemWindow.IsVisible(), "minimized:", w.SystemWindow.Is(system.Minimized)) } // 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: if w.SystemWindow.Lock() { // fmt.Printf("got close event for window %v \n", w.name) e.SetHandled() w.flags.SetFlag(true, winStopEventLoop) w.closed() w.SystemWindow.Unlock() } 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.flags.HasFlag(winGotFocus) { w.flags.SetFlag(true, winGotFocus) 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.flags.SetFlag(false, winGotFocus) 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 // 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 // TextShaper is the text shaping system for the render context, // for doing text layout. textShaper shaped.Shaper // render 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 [WidgetBase.AsyncLock] from any outside routine to grab the lock before // doing modifications. sync.Mutex } // 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 rc.textShaper = shaped.NewShaper() return rc } func (rc *renderContext) String() string { str := fmt.Sprintf("Geom: %s Visible: %v", rc.geom, rc.visible) return str } func (w *renderWindow) renderContext() *renderContext { return w.mains.renderContext } //////// renderWindow // renderWindow performs all rendering based on current Stages config. // It locks and unlocks the renderContext itself, which is necessary so that // there is a moment for other goroutines to acquire the lock and get necessary // updates through (such as in offscreen testing). func (w *renderWindow) renderWindow() { if w.flags.HasFlag(winIsRendering) { // still doing the last one w.flags.SetFlag(true, winRenderSkipped) if DebugSettings.WindowRenderTrace { log.Printf("RenderWindow: still rendering, skipped: %v\n", w.name) } return } offscreen := TheApp.Platform() == system.Offscreen sinceResize := time.Since(w.lastResize) if !offscreen && sinceResize < 100*time.Millisecond { // get many rapid updates during resizing, so just rerender last one if so. // this works best in practice after a lot of experimentation. w.flags.SetFlag(true, winRenderSkipped) w.SystemWindow.Composer().Redraw() return } 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 } spriteMods := top.Sprites.IsModified() if !spriteMods && !rebuild && !stageMods && !sceneMods { // nothing to do! if w.flags.HasFlag(winRenderSkipped) { w.flags.SetFlag(false, winRenderSkipped) } else { return } } if !w.isVisible() || w.SystemWindow.Is(system.Minimized) { if DebugSettings.WindowRenderTrace { log.Printf("RenderWindow: skipping update on inactive / minimized window: %v\n", w.name) } return } if DebugSettings.WindowRenderTrace { log.Println("RenderWindow: doing render:", w.name) log.Println("rebuild:", rebuild, "stageMods:", stageMods, "sceneMods:", sceneMods) } if !w.SystemWindow.Lock() { if DebugSettings.WindowRenderTrace { log.Printf("RenderWindow: window was closed: %v\n", w.name) } return } // now we go in the proper bottom-up order to generate the [render.Scene] cp := w.SystemWindow.Composer() cp.Start() sm := &w.mains n := sm.stack.Len() w.fillInsets(cp) // only does something on non-js // 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 { log.Println("GatherScenes: main Window:", st.String()) } winScene = st.Scene winIndex = i cp.Add(winScene.RenderSource(draw.Src), winScene) for _, dr := range winScene.directRenders { cp.Add(dr.RenderSource(draw.Over), dr) } break } } // then add everyone above that for i := winIndex + 1; i < n; i++ { st := sm.stack.ValueByIndex(i) if st.Scrim && i == n-1 { cp.Add(ScrimSource(winScene.Geom.TotalBBox), &st.Scrim) } cp.Add(st.Scene.RenderSource(draw.Over), st.Scene) if DebugSettings.WindowRenderTrace { log.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 cp.Add(st.Scene.RenderSource(draw.Over), st.Scene) if DebugSettings.WindowRenderTrace { log.Println("GatherScenes: popup:", st.String()) } } scpos := winScene.SceneGeom.Pos if TheApp.Platform().IsMobile() { scpos = image.Point{} } cp.Add(SpritesSource(&top.Sprites, scpos), &top.Sprites) w.SystemWindow.Unlock() if offscreen || w.flags.HasFlag(winResize) || sinceResize < 500*time.Millisecond { w.flags.SetFlag(true, winIsRendering) w.renderAsync(cp) if w.flags.HasFlag(winResize) { w.lastResize = time.Now() } w.flags.SetFlag(false, winResize) } else { // note: it is critical to set *before* going into loop // because otherwise we can lose an entire pass before the goroutine starts! // function will turn flag off when it finishes. w.flags.SetFlag(true, winIsRendering) go w.renderAsync(cp) } } // renderAsync is the implementation of the main render pass, // which must be called in a goroutine. It relies on the platform-specific // [renderWindow.doRender]. func (w *renderWindow) renderAsync(cp composer.Composer) { if !w.SystemWindow.Lock() { w.flags.SetFlag(false, winIsRendering) // note: comes in with flag set // fmt.Println("renderAsync SystemWindow lock fail") return } // pr := profile.Start("Compose") // fmt.Println("start compose") cp.Compose() // pr.End() w.flags.SetFlag(false, winIsRendering) // note: comes in with flag set w.SystemWindow.Unlock() } // RenderSource returns the [render.Render] state from the [Scene.Painter]. func (sc *Scene) RenderSource(op draw.Op) composer.Source { sc.setFlag(false, sceneImageUpdated) return SceneSource(sc, op) } // renderWindowFlags are atomic bit flags for [renderWindow] state. // They must be atomic to prevent race conditions. type renderWindowFlags int64 //enums:bitflag -trim-prefix win const ( // winIsRendering indicates that the renderAsync function is running. winIsRendering renderWindowFlags = iota // winRenderSkipped indicates that a render update was skipped, so // another update will be run to ensure full updating. winRenderSkipped // winResize indicates that the window was just resized. winResize // winStopEventLoop indicates that the event loop should be stopped. winStopEventLoop // winClosing is whether the window is closing. winClosing // winGotFocus indicates that have we received focus. winGotFocus ) // 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/paint/render" "cogentcore.org/core/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/styles/sides" "cogentcore.org/core/styles/units" "cogentcore.org/core/system" "cogentcore.org/core/text/shaped" "cogentcore.org/core/tree" ) // Scene contains a [Widget] tree, rooted in an embedded [Frame] layout, // which renders into its own [paint.Painter]. 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 sides.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:"-"` // painter for rendering Painter paint.Painter `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:"-"` // Animations are the currently active [Animation]s in this scene. Animations []*Animation `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:"-"` // source renderer for rendering the scene renderer render.Renderer `copier:"-" json:"-" xml:"-" display:"-" set:"-"` // 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 Painter. 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.SetAbilities(true, abilities.Clickable) // this is critical to enable click-off to turn off focus. 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.Lock() defer sm.Unlock() 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() { if TheApp.Platform() == system.Offscreen { return } // 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 } // TextShaper returns the current [shaped.TextShaper], for text shaping. // may be nil if not yet initialized. func (sc *Scene) TextShaper() shaped.Shaper { rc := sc.renderContext() if rc != nil { return rc.textShaper } return nil } // 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 and returns whether it actually needed to // be resized. func (sc *Scene) fitInWindow(winGeom math32.Geom2DInt) bool { geom := sc.SceneGeom geom = geom.FitInWindow(winGeom) return sc.resize(geom) } // resize resizes the scene if needed, creating a new image; updates Geom. // returns false if the scene is already the correct size. func (sc *Scene) resize(geom math32.Geom2DInt) bool { if geom.Size.X <= 0 || geom.Size.Y <= 0 { return false } sz := math32.FromPoint(geom.Size) if sc.Painter.State == nil { sc.Painter = *paint.NewPainter(sz) sc.Painter.Paint.UnitContext = sc.Styles.UnitContext } sc.SceneGeom.Pos = geom.Pos if sc.renderer != nil { img := sc.renderer.Image() if img != nil { isz := img.Bounds().Size() if isz == geom.Size { return false } } } else { sc.renderer = paint.NewSourceRenderer(sz) } sc.Painter.Paint.UnitContext = sc.Styles.UnitContext sc.Painter.State.Init(sc.Painter.Paint, sz) sc.renderer.SetSize(units.UnitDot, sz) sc.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() return true } // 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.flags.HasFlag(winClosing) && !mm.renderWindow.flags.HasFlag(winStopEventLoop) && !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.Geom.Size.Alloc = sc.Geom.Size.Alloc 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(10) // 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) sbwb := sbw + fr.Styles.Border.Width.Right.Dots + fr.Styles.Margin.Right.Dots od := d.Other() bbmin := math32.FromPoint(fr.Geom.ContentBBox.Min) bbmax := math32.FromPoint(fr.Geom.ContentBBox.Max) bbtmax := math32.FromPoint(fr.Geom.TotalBBox.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)) bbtmax.SetMin(math32.FromPoint(fr.Scene.Geom.TotalBBox.Max)) } pos.SetDim(d, bbmin.Dim(d)) pos.SetDim(od, bbtmax.Dim(od)-sbwb) // base from total 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.Min = 0 sb.Step = 1 sb.PageStep = float32(fr.Geom.ContentBBox.Dy()) } // 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)) } // ScrollThisToTop tells this widget's parent frame to scroll so the top // of this widget is at the top of the visible range. // It returns whether any scrolling was done. func (wb *WidgetBase) ScrollThisToTop() bool { if wb.This == nil { return false } fr := wb.parentScrollFrame() if fr == nil { return false } box := wb.AsWidget().Geom.totalRect() return fr.ScrollDimToStart(math32.Y, box.Min.Y) } // 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 a position value // relative to the visible dimensions of the frame // (i.e., subtracting ed.Geom.Pos.Content). 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 fromMax := ssz - pos // distance from max in visible window if pos < 0 || pos < math32.Abs(fromMax) { // pushing toward min pct := pos / ssz if pct < .1 && sb.Value > 0 { dst = min(dst, sb.Value) sb.setValueEvent(sb.Value - dst) return true } } else { pct := fromMax / 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 } 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). // See also [Frame.IsDimAtContentStart]. 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 } // IsDimAtContentStart returns whether the given dimension is scrolled to the // start of its content. See also [Frame.ScrollDimToContentStart]. func (fr *Frame) IsDimAtContentStart(d math32.Dims) bool { if !fr.HasScroll[d] || fr.Scrolls[d] == nil { return false } sb := fr.Scrolls[d] return sb.Value == 0 } // 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). // See also [Frame.IsDimAtContentEnd]. 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 } // IsDimAtContentEnd returns whether the given dimension is scrolled to the // end of its content. See also [Frame.ScrollDimToContentEnd]. func (fr *Frame) IsDimAtContentEnd(d math32.Dims) bool { if !fr.HasScroll[d] || fr.Scrolls[d] == nil { return false } sb := fr.Scrolls[d] return sb.Value == sb.effectiveMax() } // 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/system" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" "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%%"` // Font size factor applied only to documentation and other // dense text contexts, not normal interactive elements. // It is a percentage of the base Font size setting (higher numbers // lead to larger text). DocsFontSize 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"` // Text specifies text settings including the language, and the // font families for different styles of fonts. Text rich.Settings } func (as *AppearanceSettingsData) Defaults() { as.Text.Defaults() } // 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" } rich.DefaultSettings = as.Text // 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:"250ms" 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 text.EditorSettings // whether to use a 24-hour clock (instead of AM and PM) Clock24 bool `label:"24-hour clock"` // SnackbarTimeout is the default amount of time until snackbars // disappear (snackbars show short updates about app processes // at the bottom of the screen) SnackbarTimeout time.Duration `default:"5s"` // only support closing the currently selected active tab; // if this is set to true, pressing the close button on other tabs // will take you to that tab, from which you can close it. OnlyCloseActiveTab bool `default:"false"` // 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"` // 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 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 } //////// 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.SlideStart, func(e events.Event) { pos := sr.pointToRelPos(e.Pos()) sr.setSliderPosEvent(pos) sr.slideStartPos = sr.pos }) 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)) } }) sr.On(events.SlideStop, 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)) } sr.sendChange() }) sr.On(events.Click, func(e events.Event) { pos := sr.pointToRelPos(e.Pos()) sr.setSliderPosEvent(pos) sr.sendChange() }) 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.Painter 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.StandardBox(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.Fill.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.Fill.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.Fill.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.Fill.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() if !pwb.IsVisible() { 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.SetTextWrap(false) 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) s.Direction = styles.Column }) } }) 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() _, hasNonSpans := sl.tilesTotal() if !hasNonSpans { 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] } return sl.SubSplits[i][1+ri] case TileSecondLong: if ri == 2 { return sl.SubSplits[i][1] } 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 } 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) if nt == 0 { return } 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.StartRender() { 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.EndRender() } } // 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" "cogentcore.org/core/base/ordmap" "cogentcore.org/core/events" "cogentcore.org/core/math32" "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() scimg := wb.Scene.renderer.Image() // todo: need to make this real on JS if scimg == nil { return nil } if wb.Geom.TotalBBox.Empty() { // the widget is offscreen return nil } sz := wb.Geom.TotalBBox.Size() img := image.NewRGBA(image.Rectangle{Max: sz}) draw.Draw(img, img.Bounds(), scimg, wb.Geom.TotalBBox.Min, draw.Src) return img } // 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 sync.Mutex } // Add adds sprite to list, and returns the image index and // layer index within that for given sprite. If name already // exists on list, then it is returned, with size allocation // updated as needed. func (ss *Sprites) Add(sp *Sprite) { ss.Lock() ss.Init() ss.Map.Add(sp.Name, sp) ss.modified = true ss.Unlock() } // Delete deletes sprite by name, returning indexes where it was located. // All sprite images must be updated when this occurs, as indexes may have shifted. func (ss *Sprites) Delete(sp *Sprite) { ss.Lock() ss.DeleteKey(sp.Name) ss.modified = true ss.Unlock() } // SpriteByName returns the sprite by name func (ss *Sprites) SpriteByName(name string) (*Sprite, bool) { ss.Lock() defer ss.Unlock() return ss.ValueByKeyTry(name) } // reset removes all sprites func (ss *Sprites) reset() { ss.Lock() ss.Reset() ss.modified = true ss.Unlock() } // ActivateSprite flags the sprite as active, setting Modified if wasn't before. func (ss *Sprites) ActivateSprite(name string) { sp, ok := ss.SpriteByName(name) if !ok { return // not worth bothering about errs -- use a consistent string var! } ss.Lock() if !sp.Active { sp.Active = true ss.modified = true } ss.Unlock() } // InactivateSprite flags the sprite as inactive, setting Modified if wasn't before. func (ss *Sprites) InactivateSprite(name string) { sp, ok := ss.SpriteByName(name) if !ok { return // not worth bothering about errs -- use a consistent string var! } ss.Lock() if sp.Active { sp.Active = false ss.modified = true } ss.Unlock() } // IsModified returns whether the sprites have been modified. func (ss *Sprites) IsModified() bool { ss.Lock() defer ss.Unlock() return ss.modified } // 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. // For dialogs, this position is the target center position, not the upper-left corner. 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 stageMods || sceneMods { // fmt.Println("scene mod", st.Scene.Name, stageMods, scMods) // } 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" "cogentcore.org/core/system" ) // 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. sync.Mutex } // top returns the top-most Stage in the Stack, under Read Lock func (sm *stages) top() *Stage { sm.Lock() defer sm.Unlock() 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.Lock() defer sm.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.Lock() defer sm.Unlock() l := sm.stack.Len() fullWindow := st.FullWindow got := false 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() got = true break } } if !got { return false } // After closing a full window stage on web, the top stage behind // needs to be rerendered, or else nothing will show up. if fullWindow && TheApp.Platform() == system.Web { sz := sm.renderWindow.mains.stack.Len() if sz > 0 { ts := sm.renderWindow.mains.stack.ValueByIndex(sz - 1) if ts.Scene != nil { ts.Scene.NeedsRender() } } } return true } // 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.Lock() defer sm.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.Lock() defer sm.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.Lock() defer sm.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.Lock() defer sm.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. // if nothing actually needed to be resized, it returns false. func (sm *stages) resize(rg math32.Geom2DInt) bool { resized := false for _, kv := range sm.stack.Order { st := kv.Value if st.FullWindow { did := st.Scene.resize(rg) if did { st.Sprites.reset() resized = true } } else { did := st.Scene.fitInWindow(rg) if did { resized = true } } } return resized } // updateAll is the primary updating function to update all scenes // and determine if any updates were actually made. // This [stages] is the mains of the [renderWindow] or the popups // of a list of popups within a main stage. // It 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 lock so nothing else can happen. func (sm *stages) updateAll() (stageMods, sceneMods bool) { sm.Lock() defer sm.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() } } // If we own popups, we also need to runDeferred on them. if st.Main == st && st.popups.stack.Len() > 0 { st.popups.runDeferred() } } } // 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/styles" "cogentcore.org/core/styles/states" "cogentcore.org/core/text/rich" "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.Font.Family = rich.SansSerif } // 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) { var rc *renderContext sz := image.Point{1920, 1080} if sc != nil { 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) st.Font.ToDots(&st.UnitContext) // key to set first st.Font.SetUnitContext(&st.UnitContext) st.ToDots() } // ChildBackground returns the background color (Image) for the 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]) 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/base/iox/imagex" "cogentcore.org/core/cursors" "cogentcore.org/core/events" "cogentcore.org/core/icons" "cogentcore.org/core/math32" "cogentcore.org/core/paint" "cogentcore.org/core/paint/render" "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" ) // todo: rewrite svg.SVG to accept an external painter to render to, // and use that for this, so it renders directly instead of via image. // 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:"-"` // image renderer renderer render.Renderer // cached rendered image image image.Image // prevSize is the cached allocated size for the last rendered image. prevSize image.Point `xml:"-" json:"-" set:"-"` } func (sv *SVG) Init() { sv.WidgetBase.Init() sz := math32.Vec2(10, 10) sv.SVG = svg.NewSVG(sz) sv.renderer = paint.NewImageRenderer(sz) 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)) } // SaveImage saves the current rendered SVG image to an image file, // using the filename extension to determine the file type. func (sv *SVG) SaveImage(filename Filename) error { //types:add return sv.SVG.SaveImage(string(filename)) } func (sv *SVG) SizeFinal() { sv.WidgetBase.SizeFinal() sz := sv.Geom.Size.Actual.Content sv.SVG.SetSize(sz) sv.renderer.SetSize(units.UnitDot, sz) } // renderSVG renders the SVG func (sv *SVG) renderSVG() { if sv.SVG == nil { return } sv.SVG.TextShaper = sv.Scene.TextShaper() sv.renderer.Render(sv.SVG.Render(nil).RenderDone()) sv.image = imagex.WrapJS(sv.renderer.Image()) sv.prevSize = sv.image.Bounds().Size() } func (sv *SVG) Render() { sv.WidgetBase.Render() if sv.SVG == nil { return } needsRender := !sv.IsReadOnly() if !needsRender { if sv.image == nil { needsRender = true } else { sz := sv.image.Bounds().Size() if sz != sv.prevSize || sz == (image.Point{}) { needsRender = true } } } if needsRender { sv.renderSVG() } r := sv.Geom.ContentBBox sp := sv.Geom.ScrollOffset() sv.Scene.Painter.DrawImage(sv.image, r, 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.SaveImage).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/text/text" "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) { if !sw.IsReadOnly() { s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Hoverable, abilities.Checkable) s.Cursor = cursors.Pointer } s.Text.Align = text.Start s.Text.AlignV = text.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.SendClickOnEnter() sw.OnFinal(events.Click, func(e events.Event) { if sw.IsReadOnly() { return } 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 } updated := false for fli := 0; fli < tb.numVisibleFields; fli++ { field := tb.visibleFields[fli] val := tb.sliceElementValue(0) fval := val.FieldByIndex(field.Index) isString := fval.Type().Kind() == reflect.String && fval.Type() != reflect.TypeFor[icons.Icon]() if !isString { tb.colMaxWidths[fli] = 0 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)) } if mxw != tb.colMaxWidths[fli] { tb.colMaxWidths[fli] = mxw updated = true } } if updated { tb.Update() } } 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.RunWindowDialog(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" ) // Tabber is an interface for getting the parent Tabs of tab buttons. type Tabber interface { // AsCoreTabs returns the underlying Tabs implementation. AsCoreTabs() *Tabs } // 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) AsCoreTabs() *Tabs { return ts } 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) }) }) // 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) { // tab frames must scroll independently and grow s.Overflow.Set(styles.OverflowAuto) s.Grow.Set(1, 1) 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.SendClickOnEnter() 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 { if tbr, ok := tb.Parent.AsTree().Parent.(Tabber); ok { return tbr.AsCoreTabs() } return nil } func (tb *Tab) Label() string { if tb.Text != "" { return tb.Text } return tb.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 core import ( "image" "cogentcore.org/core/base/iox/imagex" "cogentcore.org/core/events" "cogentcore.org/core/system/composer" ) // getImager is implemented by offscreen.Drawer for [Body.AssertRender]. type getImager interface { GetImage() *image.RGBA } // 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. func (b *Body) AssertRender(t imagex.TestingT, filename string, fun ...func()) { b.runAndShowNewWindow() rw := b.Scene.RenderWindow() for i := 0; i < len(fun); i++ { fun[i]() b.waitNoEvents(rw) } if len(fun) == 0 { // we didn't get it above b.waitNoEvents(rw) } // Ensure that everything is updated and rendered. If there are no changes, // the performance impact is minimal. for range 10 { // note: 10 is essential for textcore tests rw.renderWindow() b.AsyncLock() rw.mains.runDeferred() b.AsyncUnlock() } dw := b.Scene.RenderWindow().SystemWindow.Composer().(*composer.ComposerDrawer).Drawer img := dw.(getImager).GetImage() imagex.Assert(t, img, filename) // When closing the scene, our access to the render context stops working, // so using normal AsyncLock and AsyncUnlock will lead to AsyncLock failing. // That leaves the lock on, which prevents the WinClose event from being // received. Therefore, we get the rc ahead of time. rc := b.Scene.renderContext() rc.Lock() b.Close() rc.Unlock() } // 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 *renderWindow) { rw.noEventsChan = make(chan struct{}) <-rw.noEventsChan rw.noEventsChan = 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 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/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/system" "cogentcore.org/core/text/htmltext" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/text" "cogentcore.org/core/text/textpos" ) // 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 // Links is the list of links in the text. Links []rich.Hyperlink `copier:"-" json:"-" xml:"-" set:"-"` // richText is the conversion of the HTML text source. richText rich.Text // paintText is the [shaped.Lines] for the text. paintText *shaped.Lines // normalCursor is the cached cursor to display when there // is no link being hovered. normalCursor cursors.Cursor // selectRange is the selected range, in _runes_, which must be applied selectRange textpos.Range } // 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.AddContextMenu(tx.contextMenu) tx.SetType(TextBodyLarge) tx.Styler(func(s *styles.Style) { s.SetAbilities(true, abilities.Selectable, abilities.DoubleClickable, abilities.TripleClickable, abilities.LongPressable, abilities.Slideable) if len(tx.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 = 20.0 / 14 s.Font.Size.Dp(14) s.Font.Weight = rich.Medium case TextLabelMedium: s.Text.LineHeight = 16.0 / 12 s.Font.Size.Dp(12) s.Font.Weight = rich.Medium case TextLabelSmall: s.Text.LineHeight = 16.0 / 11 s.Font.Size.Dp(11) s.Font.Weight = rich.Medium case TextBodyLarge: s.Text.LineHeight = 24.0 / 16 s.Font.Size.Dp(16) s.Font.Weight = rich.Normal case TextSupporting: s.Color = colors.Scheme.OnSurfaceVariant fallthrough case TextBodyMedium: s.Text.LineHeight = 20.0 / 14 s.Font.Size.Dp(14) s.Font.Weight = rich.Normal case TextBodySmall: s.Text.LineHeight = 16.0 / 12 s.Font.Size.Dp(12) s.Font.Weight = rich.Normal case TextTitleLarge: s.Text.LineHeight = 28.0 / 22 s.Font.Size.Dp(22) s.Font.Weight = rich.Normal case TextTitleMedium: s.Text.LineHeight = 24.0 / 16 s.Font.Size.Dp(16) s.Font.Weight = rich.Bold case TextTitleSmall: s.Text.LineHeight = 20.0 / 14 s.Font.Size.Dp(14) s.Font.Weight = rich.Medium case TextHeadlineLarge: s.Text.LineHeight = 40.0 / 32 s.Font.Size.Dp(32) s.Font.Weight = rich.Normal case TextHeadlineMedium: s.Text.LineHeight = 36.0 / 28 s.Font.Size.Dp(28) s.Font.Weight = rich.Normal case TextHeadlineSmall: s.Text.LineHeight = 32.0 / 24 s.Font.Size.Dp(24) s.Font.Weight = rich.Normal case TextDisplayLarge: s.Text.LineHeight = 70.0 / 57 s.Font.Size.Dp(57) s.Font.Weight = rich.Normal case TextDisplayMedium: s.Text.LineHeight = 52.0 / 45 s.Font.Size.Dp(45) s.Font.Weight = rich.Normal case TextDisplaySmall: s.Text.LineHeight = 44.0 / 36 s.Font.Size.Dp(36) s.Font.Weight = rich.Normal } }) tx.FinalStyler(func(s *styles.Style) { tx.normalCursor = s.Cursor tx.updateRichText() // note: critical to update with final styles if tx.paintText != nil && tx.Text != "" { _, tsty := s.NewRichText() tx.paintText.UpdateStyle(tx.richText, tsty) } }) tx.HandleTextClick(func(tl *rich.Hyperlink) { system.TheApp.OpenURL(tl.URL) }) tx.OnFocusLost(func(e events.Event) { tx.selectReset() }) tx.OnKeyChord(func(e events.Event) { if tx.selectRange.Len() == 0 { 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 } }) tx.On(events.DoubleClick, func(e events.Event) { e.SetHandled() tx.selectWord(tx.pixelToRune(e.Pos())) tx.SetFocusQuiet() }) tx.On(events.TripleClick, func(e events.Event) { e.SetHandled() tx.selectAll() tx.SetFocusQuiet() if TheApp.SystemPlatform().IsMobile() { tx.Send(events.ContextMenu, e) } }) tx.On(events.SlideStart, func(e events.Event) { e.SetHandled() tx.SetState(true, states.Sliding) tx.SetFocusQuiet() tx.selectRange.Start = tx.pixelToRune(e.Pos()) tx.selectRange.End = tx.selectRange.Start tx.paintText.SelectReset() tx.NeedsRender() }) tx.On(events.SlideMove, func(e events.Event) { e.SetHandled() tx.selectUpdate(tx.pixelToRune(e.Pos())) tx.NeedsRender() }) tx.On(events.SlideStop, func(e events.Event) { if TheApp.SystemPlatform().IsMobile() { tx.Send(events.ContextMenu, e) } }) tx.FinalUpdater(func() { tx.updateRichText() tx.configTextAlloc(tx.Geom.Size.Alloc.Content) }) } // updateRichText gets the richtext from Text, using HTML parsing. func (tx *Text) updateRichText() { sty, tsty := tx.Styles.NewRichText() if tsty.WhiteSpace.KeepWhiteSpace() { tx.richText, _ = htmltext.HTMLPreToRich([]byte(tx.Text), sty, nil) } else { tx.richText, _ = htmltext.HTMLToRich([]byte(tx.Text), sty, nil) } tx.Links = tx.richText.GetLinks() } // 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) (*rich.Hyperlink, image.Rectangle) { if tx.paintText == nil || len(tx.Links) == 0 { return nil, image.Rectangle{} } tpos := tx.Geom.Pos.Content ri := tx.pixelToRune(pos) for li := range tx.Links { lr := &tx.Links[li] if !lr.Range.Contains(ri) { continue } gb := tx.paintText.RuneBounds(ri).Translate(tpos).ToRect() return lr, gb } 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 *rich.Hyperlink)) { 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) contextMenu(m *Scene) { NewFuncButton(m).SetFunc(tx.copy).SetIcon(icons.Copy).SetKey(keymap.Copy).SetEnabled(tx.hasSelection()) } func (tx *Text) copy() { //types:add if !tx.hasSelection() { return } // note: selectRange is in runes, not string indexes. md := mimedata.NewText(string([]rune(tx.Text)[tx.selectRange.Start:tx.selectRange.End])) em := tx.Events() if em != nil { em.Clipboard().Write(md) } tx.selectReset() } func (tx *Text) Label() string { if tx.Text != "" { return tx.Text } return tx.Name } func (tx *Text) pixelToRune(pt image.Point) int { return tx.paintText.RuneAtPoint(math32.FromPoint(pt), tx.Geom.Pos.Content) } // selectUpdate updates selection based on rune index func (tx *Text) selectUpdate(ri int) { if ri >= tx.selectRange.Start { tx.selectRange.End = ri } else { tx.selectRange.Start, tx.selectRange.End = ri, tx.selectRange.Start } tx.paintText.SelectReset() tx.paintText.SelectRegion(tx.selectRange) } // hasSelection returns true if there is an active selection. func (tx *Text) hasSelection() bool { return tx.selectRange.Len() > 0 } // selectReset resets any current selection func (tx *Text) selectReset() { tx.selectRange.Start = 0 tx.selectRange.End = 0 tx.paintText.SelectReset() tx.NeedsRender() } // selectAll selects entire set of text func (tx *Text) selectAll() { tx.selectRange.Start = 0 txt := tx.richText.Join() tx.selectUpdate(len(txt)) tx.NeedsRender() } // selectWord selects word at given rune location func (tx *Text) selectWord(ri int) { tx.paintText.SelectReset() txt := tx.richText.Join() wr := textpos.WordAt(txt, ri) if wr.Start >= 0 { tx.selectRange = wr tx.paintText.SelectRegion(tx.selectRange) } tx.NeedsRender() } // configTextSize does the text shaping layout for text, // using given size to constrain layout. func (tx *Text) configTextSize(sz math32.Vector2) { if tx.Styles.Font.Size.Dots == 0 { // not init return } sty, tsty := tx.Styles.NewRichText() tsty.Align, tsty.AlignV = text.Start, text.Start tx.paintText = tx.Scene.TextShaper().WrapLines(tx.richText, sty, tsty, &AppearanceSettings.Text, sz) } // 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 { if tx.Scene == nil || tx.Scene.TextShaper() == nil { return sz } if tx.Styles.Font.Size.Dots == 0 { return sz // not init } tsh := tx.Scene.TextShaper() sty, tsty := tx.Styles.NewRichText() rsz := sz if tsty.Align != text.Start && tsty.AlignV != text.Start { etxs := *tsty etxs.Align, etxs.AlignV = text.Start, text.Start tx.paintText = tsh.WrapLines(tx.richText, sty, &etxs, &AppearanceSettings.Text, rsz) rsz = tx.paintText.Bounds.Size().Ceil() } tx.paintText = tsh.WrapLines(tx.richText, sty, tsty, &AppearanceSettings.Text, rsz) return tx.paintText.Bounds.Size().Ceil() } func (tx *Text) SizeUp() { tx.WidgetBase.SizeUp() // sets Actual size based on styles sz := &tx.Geom.Size if tx.Styles.Text.WhiteSpace.HasWordWrap() { sty, tsty := tx.Styles.NewRichText() est := shaped.WrapSizeEstimate(sz.Actual.Content, len(tx.Text), .5, sty, tsty) tx.configTextSize(est) } else { tx.configTextSize(sz.Actual.Content) } if tx.paintText == nil { return } rsz := tx.paintText.Bounds.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.WhiteSpace.HasWordWrap() || iter > 1 { return false } sz := &tx.Geom.Size asz := sz.Alloc.Content rsz := tx.configTextAlloc(asz) // 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.Scene.Painter.DrawText(tx.paintText, 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/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/styles/states" "cogentcore.org/core/styles/units" "cogentcore.org/core/system" "cogentcore.org/core/text/parse/complete" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/text" "cogentcore.org/core/text/textpos" "cogentcore.org/core/tree" "golang.org/x/image/draw" ) // TextField is a widget for editing a line of text. // // 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 // 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 // dispRange is the range of visible text, for scrolling text case (non-wordwrap). dispRange textpos.Range // cursorPos is the current cursor position as rune index into string. cursorPos int // cursorLine is the current cursor line position, for word wrap case. 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 // selectRange is the selected range. selectRange textpos.Range // 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 *shaped.Lines // renderVisible is the render version of just the visible text in dispRange. renderVisible *shaped.Lines // renderedRange is the dispRange last rendered. renderedRange textpos.Range // number of lines from last render update, for word-wrap version numLines int // lineHeight is the line height cached during styling. lineHeight float32 // blinkOn oscillates between on and off for blinking. blinkOn bool // cursorMu is the mutex for updating the cursor between blinker and field. cursorMu sync.Mutex // 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) s.SetAbilities(false, abilities.ScrollableUnattended) tf.CursorWidth.Dp(1) 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.LineHeight = 1.4 s.Text.Align = text.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.OnClick(func(e events.Event) { if !tf.IsReadOnly() { tf.SetFocus() } switch e.MouseButton() { case events.Left: tf.setCursorFromPixel(e.Pos(), e.SelectMode()) case events.Middle: if !tf.IsReadOnly() { tf.paste() } } }) 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.SlideStart, func(e events.Event) { e.SetHandled() tf.SetState(true, states.Sliding) if tf.selectMode || e.SelectMode() != events.SelectOne { // extend existing select tf.setCursorFromPixel(e.Pos(), e.SelectMode()) } else { tf.cursorPos = tf.pixelToCursor(e.Pos()) if !tf.selectMode { tf.selectModeToggle() } } }) tf.On(events.SlideMove, func(e events.Event) { e.SetHandled() tf.selectMode = true // always 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 } }) tf.Updater(func() { tf.renderVisible = nil // ensures re-render }) } 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 } // textEdited must be called whenever the text is edited. // it sets the edited flag and ensures a new render of current text. func (tf *TextField) textEdited() { tf.edited = true tf.renderVisible = nil tf.NeedsRender() } // 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.renderVisible = nil tf.editText = []rune(tf.text) tf.edited = false tf.dispRange.Start = 0 tf.dispRange.End = tf.charWidth tf.selectReset() tf.NeedsRender() } // clear clears any existing text. func (tf *TextField) clear() { tf.renderVisible = nil tf.edited = true tf.editText = tf.editText[:0] tf.dispRange.Start = 0 tf.dispRange.End = 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 func (tf *TextField) updateLinePos() { tf.cursorLine = tf.renderAll.RuneToLinePos(tf.cursorPos).Line } // 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.dispRange.End { inc := tf.cursorPos - tf.dispRange.End tf.dispRange.End += inc } tf.updateLinePos() if tf.selectMode { tf.selectRegionUpdate(tf.cursorPos) } tf.NeedsRender() } // cursorForwardWord moves the cursor forward by words func (tf *TextField) cursorForwardWord(steps int) { tf.cursorPos, _ = textpos.ForwardWord(tf.editText, tf.cursorPos, steps) if tf.cursorPos > tf.dispRange.End { inc := tf.cursorPos - tf.dispRange.End tf.dispRange.End += inc } tf.updateLinePos() 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.dispRange.Start { dec := min(tf.dispRange.Start, 8) tf.dispRange.Start -= dec } tf.updateLinePos() if tf.selectMode { tf.selectRegionUpdate(tf.cursorPos) } tf.NeedsRender() } // cursorBackwardWord moves the cursor backward by words func (tf *TextField) cursorBackwardWord(steps int) { tf.cursorPos, _ = textpos.BackwardWord(tf.editText, tf.cursorPos, steps) if tf.cursorPos <= tf.dispRange.Start { dec := min(tf.dispRange.Start, 8) tf.dispRange.Start -= dec } tf.updateLinePos() 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 } tf.cursorPos = tf.renderVisible.RuneAtLineDelta(tf.cursorPos, steps) tf.updateLinePos() 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 } tf.cursorPos = tf.renderVisible.RuneAtLineDelta(tf.cursorPos, -steps) tf.updateLinePos() 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.dispRange.Start = 0 tf.dispRange.End = min(len(tf.editText), tf.dispRange.Start+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.dispRange.End = len(tf.editText) // try -- display will adjust tf.dispRange.Start = max(0, tf.dispRange.End-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.editText = append(tf.editText[:tf.cursorPos-steps], tf.editText[tf.cursorPos:]...) tf.textEdited() tf.cursorBackward(steps) } // 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.editText = append(tf.editText[:tf.cursorPos], tf.editText[tf.cursorPos+steps:]...) tf.textEdited() } // 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.editText = append(tf.editText[:tf.cursorPos], tf.editText[org:]...) tf.textEdited() } // 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.editText = append(tf.editText[:tf.cursorPos], tf.editText[org:]...) tf.textEdited() } // 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.selectRange.Start < tf.selectRange.End } // selection returns the currently selected text func (tf *TextField) selection() string { if tf.hasSelection() { return string(tf.editText[tf.selectRange.Start:tf.selectRange.End]) } 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.selectRange.Start = tf.cursorPos tf.selectRange.End = tf.selectRange.Start } } // 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.selectRange.Start = pos tf.selectRange.End = tf.selectInit } else { tf.selectRange.Start = tf.selectInit tf.selectRange.End = pos } tf.selectUpdate() } // selectAll selects all the text func (tf *TextField) selectAll() { tf.selectRange.Start = 0 tf.selectInit = 0 tf.selectRange.End = len(tf.editText) if TheApp.SystemPlatform().IsMobile() { tf.Send(events.ContextMenu) } tf.NeedsRender() } // 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.selectRange = textpos.WordAt(tf.editText, tf.cursorPos) tf.selectInit = tf.selectRange.Start if TheApp.SystemPlatform().IsMobile() { tf.Send(events.ContextMenu) } tf.NeedsRender() } // selectReset resets the selection func (tf *TextField) selectReset() { tf.selectMode = false if tf.selectRange.Start == 0 && tf.selectRange.End == 0 { return } tf.selectRange.Start = 0 tf.selectRange.End = 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.selectRange.Start < tf.selectRange.End { ed := len(tf.editText) if tf.selectRange.Start < 0 { tf.selectRange.Start = 0 } if tf.selectRange.End > ed { tf.selectRange.End = 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.editText = append(tf.editText[:tf.selectRange.Start], tf.editText[tf.selectRange.End:]...) if tf.cursorPos > tf.selectRange.Start { if tf.cursorPos < tf.selectRange.End { tf.cursorPos = tf.selectRange.Start } else { tf.cursorPos -= tf.selectRange.End - tf.selectRange.Start } } tf.textEdited() tf.selectReset() 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.selection()) 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.selectRange.Start && tf.cursorPos < tf.selectRange.End { 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() } 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.dispRange.End += rsl tf.textEdited() tf.cursorForward(rsl) } 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.renderVisible = nil 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.renderVisible = nil 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.WhiteSpace.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.Lines) == 0 { return math32.Vector2{} } bb := tf.renderAll.RuneBounds(idx) if idx >= len(tf.editText) { if tf.numLines > 1 && tf.editText[len(tf.editText)-1] == ' ' { bb.Max.X += tf.lineHeight * 0.2 return bb.Max } return bb.Max } return bb.Min } // 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.dispRange.Start, charidx) return pos.Add(cpos) } var ( // textFieldBlinker manages cursor blinking textFieldBlinker = Blinker{} // textFieldSpriteName is the name of the window sprite used for the cursor textFieldSpriteName = "TextField.Cursor" ) func init() { TheApp.AddQuitCleanFunc(textFieldBlinker.QuitClean) textFieldBlinker.Func = func() { w := textFieldBlinker.Widget textFieldBlinker.Unlock() // comes in locked if w == nil { return } tf := AsTextField(w) if !tf.StateIs(states.Focused) || !tf.IsVisible() { tf.blinkOn = false tf.renderCursor(false) } else { // Need consistent test results on offscreen. if TheApp.Platform() != system.Offscreen { tf.blinkOn = !tf.blinkOn } tf.renderCursor(tf.blinkOn) } } } // startCursor starts the cursor blinking and renders it func (tf *TextField) startCursor() { if tf == nil || tf.This == nil { 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 || tf.Scene.Stage == nil { return } ms := tf.Scene.Stage.Main if ms == nil { return } spnm := fmt.Sprintf("%v-%v", textFieldSpriteName, tf.lineHeight) ms.Sprites.InactivateSprite(spnm) 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.lineHeight) sp, ok := ms.Sprites.SpriteByName(spnm) // TODO: figure out how to update caret color on color scheme change if !ok { bbsz := image.Point{int(math32.Ceil(tf.CursorWidth.Dots)), int(math32.Ceil(tf.lineHeight))} 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() { tf.renderVisible.SelectReset() if !tf.hasSelection() { return } dn := tf.dispRange.Len() effst := max(0, tf.selectRange.Start-tf.dispRange.Start) effed := min(dn, tf.selectRange.End-tf.dispRange.Start) if effst == effed { return } // fmt.Println("sel range:", effst, effed) tf.renderVisible.SelectRegion(textpos.Range{effst, effed}) } // autoScroll scrolls the starting position to keep the cursor visible, // and does various other state-updating steps to ensure everything is updated. // This is called during Render(). func (tf *TextField) autoScroll() { sz := &tf.Geom.Size icsz := tf.iconsSize() availSz := sz.Actual.Content.Sub(icsz) if tf.renderAll != nil { availSz.Y += tf.renderAll.LineHeight * 2 // allow it to add a line } tf.configTextSize(availSz) n := len(tf.editText) tf.cursorPos = math32.Clamp(tf.cursorPos, 0, n) if tf.hasWordWrap() { // does not scroll tf.dispRange.Start = 0 tf.dispRange.End = n if len(tf.renderAll.Lines) != tf.numLines { tf.renderVisible = nil tf.NeedsLayout() } return } st := &tf.Styles if n == 0 || tf.Geom.Size.Actual.Content.X <= 0 { tf.cursorPos = 0 tf.dispRange.End = 0 tf.dispRange.Start = 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.dispRange.End == 0 || tf.dispRange.End > n { // not init tf.dispRange.End = n } if tf.dispRange.Start >= tf.dispRange.End { tf.dispRange.Start = max(0, tf.dispRange.End-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.dispRange.Start + inc) { tf.dispRange.Start -= inc tf.dispRange.Start = max(tf.dispRange.Start, 0) tf.dispRange.End = tf.dispRange.Start + tf.charWidth tf.dispRange.End = min(n, tf.dispRange.End) } else if tf.cursorPos > (tf.dispRange.End - inc) { tf.dispRange.End += inc tf.dispRange.End = min(tf.dispRange.End, n) tf.dispRange.Start = tf.dispRange.End - tf.charWidth tf.dispRange.Start = max(0, tf.dispRange.Start) startIsAnchor = false } if tf.dispRange.End < tf.dispRange.Start { return } if startIsAnchor { gotWidth := false spos := tf.charPos(tf.dispRange.Start).X for { w := tf.charPos(tf.dispRange.End).X - spos if w < maxw { if tf.dispRange.End == n { break } nw := tf.charPos(tf.dispRange.End+1).X - spos if nw >= maxw { gotWidth = true break } tf.dispRange.End++ } else { tf.dispRange.End-- } } if gotWidth || tf.dispRange.Start == 0 { return } // otherwise, try getting some more chars by moving up start.. } // end is now anchor epos := tf.charPos(tf.dispRange.End).X for { w := epos - tf.charPos(tf.dispRange.Start).X if w < maxw { if tf.dispRange.Start == 0 { break } nw := epos - tf.charPos(tf.dispRange.Start-1).X if nw >= maxw { break } tf.dispRange.Start-- } else { tf.dispRange.Start++ } } } // 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.dispRange.Start } n := len(tf.editText) if tf.hasWordWrap() { ix := tf.renderAll.RuneAtPoint(ptf, tf.effPos) if ix >= 0 { return ix } return tf.dispRange.Start } pr := tf.PointToRelPos(pt) px := float32(pr.X) st := &tf.Styles c := tf.dispRange.Start + int(float64(px/st.UnitContext.Dots(units.UnitCh))) c = min(c, n) w := tf.relCharPos(tf.dispRange.Start, c).X if w > px { for w > px { c-- if c <= tf.dispRange.Start { c = tf.dispRange.Start break } w = tf.relCharPos(tf.dispRange.Start, c).X } } else if w < px { for c < tf.dispRange.End { wn := tf.relCharPos(tf.dispRange.Start, 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.selectRange.Start = oldPos tf.selectMode = true } if !tf.StateIs(states.Sliding) && selMode == events.SelectOne { 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 { txt := tf.editText if len(txt) == 0 && len(tf.Placeholder) > 0 { txt = []rune(tf.Placeholder) } if tf.NoEcho { txt = concealDots(len(tf.editText)) } sty, tsty := tf.Styles.NewRichText() etxs := *tsty etxs.Align, etxs.AlignV = text.Start, text.Start // only works with this tx := rich.NewText(sty, txt) tf.renderAll = tf.Scene.TextShaper().WrapLines(tx, sty, &etxs, &AppearanceSettings.Text, sz) rsz := tf.renderAll.Bounds.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.renderVisible = nil tf.Frame.SizeUp() txt := tf.editText if len(txt) == 0 && len(tf.Placeholder) > 0 { txt = []rune(tf.Placeholder) } tf.dispRange.Start = 0 tf.dispRange.End = len(txt) sz := &tf.Geom.Size icsz := tf.iconsSize() availSz := sz.Actual.Content.Sub(icsz) rsz := tf.configTextSize(availSz) rsz.SetAdd(icsz) sz.FitSizeMax(&sz.Actual.Content, rsz) sz.setTotalFromContent(&sz.Actual) tf.lineHeight = tf.Styles.LineHeightDots() if DebugSettings.LayoutTrace { fmt.Println(tf, "TextField SizeUp:", rsz, "Actual:", sz.Actual.Content) } } func (tf *TextField) SizeDown(iter int) bool { sz := &tf.Geom.Size prevContent := sz.Actual.Content sz.setInitContentMin(tf.Styles.Min.Dots().Ceil()) pgrow, _ := tf.growToAllocSize(sz.Actual.Content, sz.Alloc.Content) // get before update icsz := tf.iconsSize() availSz := pgrow.Sub(icsz) rsz := tf.configTextSize(availSz) rsz.SetAdd(icsz) // start over so we don't reflect hysteresis of prior guess chg := prevContent != sz.Actual.Content if chg { if DebugSettings.LayoutTrace { fmt.Println(tf, "TextField Size Changed:", sz.Actual.Content, "was:", prevContent) } } if tf.Styles.Grow.X > 0 { rsz.X = max(pgrow.X, rsz.X) } if tf.Styles.Grow.Y > 0 { rsz.Y = max(pgrow.Y, rsz.Y) } sz.FitSizeMax(&sz.Actual.Content, rsz) sz.setTotalFromContent(&sz.Actual) sz.Alloc = sz.Actual // this is important for constraining our children layout: redo := tf.Frame.SizeDown(iter) return chg || redo } func (tf *TextField) SizeFinal() { tf.Geom.RelPos.SetZero() // tf.sizeFromChildrenFit(0, SizeFinalPass) // key to omit tf.growToAlloc() tf.sizeFinalChildren() tf.styleSizeUpdate() // now that sizes are stable, ensure styling based on size is updated tf.sizeFinalParts() } 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 } if tf.renderAll == nil { tf.numLines = 0 } else { tf.numLines = len(tf.renderAll.Lines) } if tf.numLines <= 1 { pos.Y += 0.5 * (sz.Y - tf.lineHeight) // center } tf.effSize = sz.Ceil() tf.effPos = pos.Ceil() } func (tf *TextField) layoutCurrent() { cur := tf.editText[tf.dispRange.Start:tf.dispRange.End] clr := tf.Styles.Color if len(tf.editText) == 0 && len(tf.Placeholder) > 0 { clr = tf.PlaceholderColor 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) sty, tsty := tf.Styles.NewRichText() tsty.Color = colors.ToUniform(clr) tx := rich.NewText(sty, cur) tf.renderVisible = tf.Scene.TextShaper().WrapLines(tx, sty, tsty, &AppearanceSettings.Text, availSz) tf.renderedRange = tf.dispRange } func (tf *TextField) Render() { defer func() { if tf.IsReadOnly() { return } if tf.StateIs(states.Focused) { tf.startCursor() } else { tf.stopCursor() } }() tf.autoScroll() // does all update checking, inits paint with our style tf.RenderAllocBox() if tf.dispRange.Start < 0 || tf.dispRange.End > len(tf.editText) { return } if tf.renderVisible == nil || tf.dispRange != tf.renderedRange { tf.layoutCurrent() } tf.renderSelect() tf.Scene.Painter.DrawText(tf.renderVisible, tf.effPos) } // 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(59).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]. All toolbars use the // [WidgetBase.Maker] system, so you cannot directly add widgets; see // https://cogentcore.org/core/toolbar. 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/text/text" "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 toggled open. // The base version does nothing. OnOpen() // OnClose is called when a node is toggled 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 [Tree] if it has // an AsCoreTree() method, 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; // select events are sent to both selected nodes and the root node. // See [Tree.IsRootSelected] to check whether a select event on the root // node corresponds to the root node or another node. 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:"-" json:"-" xml:"-"` // 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 Treer `copier:"-" json:"-" xml:"-" edit:"-" set:"-"` // 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 if tvn.Root == nil { 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 = text.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) { tr.This.(Treer).DropDeleteSource(e) }) 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 { if tr.Root == nil { tr.Root = tr } } troot := tr.Root.AsCoreTree() if troot.TreeInit != nil { troot.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.AsCoreTree().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.IsRoot() { // do it every time on root tr.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() { if tr.Root == nil { slog.Error("core.Tree: RootView is nil", "in node:", tr) return } rn := tr.Root.AsCoreTree() 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 sz.Alloc = sz.Actual psz := &tr.Parts.Geom.Size psz.Alloc.Total = tr.widgetSize psz.setContentFromTotal(&psz.Alloc) 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() sz.Actual.Total = tr.widgetSize // key: we revert to just ourselves } func (tr *Tree) Render() { pc := &tr.Scene.Painter 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.StandardBox(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.StartRender() { 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.EndRender() } // We have to render our children outside of `if StartRender` // since we could be out of scope but they could still be in! if !tr.Closed { tr.renderChildren() } } //////// Selection // IsRootSelected returns whether the root node is the only node selected. // This can be used in [events.Select] event handlers to check whether a // select event on the root node truly corresponds to the root node or whether // it is for another node, as select events are sent to the root when any node // is selected. func (tr *Tree) IsRootSelected() bool { return len(tr.SelectedNodes) == 1 && tr.SelectedNodes[0] == tr.Root } // 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 } rn := tr.Root.AsCoreTree() if len(rn.SelectedNodes) == 0 { return rn.SelectedNodes } return rn.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.AsCoreTree().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.AsCoreTree() 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.IsRoot() { tr.Send(events.Select, original...) } tr.Root.AsCoreTree().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.IsRoot() { tr.SendChange(original...) } tr.Root.AsCoreTree().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...) rn := tr.Root.AsCoreTree() if rn.SyncNode != nil { rn.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) } 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) } 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 { rn := tr.Root.AsCoreTree() rn.selectUpdate(selMode) rn.SetFocusQuiet() rn.ScrollToThis() rn.sendSelectEvent() return rn } // 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.AsCoreTree().This { if len(action) > 0 { MessageSnackbar(tr, fmt.Sprintf("Cannot %v the root of the tree", action[0])) } 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.AsCoreTree()))) 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() rn := tr.Root.AsCoreTree() tr.UnselectAll() for _, sn := range sels { sn.AsTree().Delete() } rn.Update() rn.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() { 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.dragStateReset() tr.Parts.dragStateReset() 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) rn := tr.Root.AsCoreTree() for _, d := range md { if d.Type != fileinfo.TextPlain { // link continue } path := string(d.Data) sn := rn.FindPath(path) if sn != nil { sn.AsTree().Delete() } sn = rn.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.AsCoreTree().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.AsCoreTree().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.AsCoreTree().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.AsCoreTree().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/styles/units" "cogentcore.org/core/text/parse/complete" "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: "painter", Doc: "painter is the paint painter 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.Painter)) *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.Collapser", IDName: "collapser", Doc: "Collapser is a widget that can be collapsed or expanded by a user.\nThe [Collapser.Summary] is always visible, and the [Collapser.Details]\nare only visible when the [Collapser] is expanded with [Collapser.Open]\nequal to true.\n\nYou can directly add any widgets to the [Collapser.Summary] and [Collapser.Details]\nby specifying one of them as the parent in calls to New{WidgetName}.\nCollapser is similar to HTML's <details> and <summary> tags.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Open", Doc: "Open is whether the collapser is currently expanded. It defaults to false."}, {Name: "Summary", Doc: "Summary is the part of the collapser that is always visible."}, {Name: "Details", Doc: "Details is the part of the collapser that is only visible when\nthe collapser is expanded."}}}) // NewCollapser returns a new [Collapser] with the given optional parent: // Collapser is a widget that can be collapsed or expanded by a user. // The [Collapser.Summary] is always visible, and the [Collapser.Details] // are only visible when the [Collapser] is expanded with [Collapser.Open] // equal to true. // // You can directly add any widgets to the [Collapser.Summary] and [Collapser.Details] // by specifying one of them as the parent in calls to New{WidgetName}. // Collapser is similar to HTML's <details> and <summary> tags. func NewCollapser(parent ...tree.Node) *Collapser { return tree.New[Collapser](parent...) } // SetOpen sets the [Collapser.Open]: // Open is whether the collapser is currently expanded. It defaults to false. func (t *Collapser) SetOpen(v bool) *Collapser { t.Open = 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: "Modified", Doc: "Modified optionally highlights and tracks fields that have been modified\nthrough an OnChange event. If present, it replaces the default value highlighting\nand resetting logic. Ignored if nil."}, {Name: "structFields", Doc: "structFields are the fields of the current struct, keys are field paths."}, {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 } // SetModified sets the [Form.Modified]: // Modified optionally highlights and tracks fields that have been modified // through an OnChange event. If present, it replaces the default value highlighting // and resetting logic. Ignored if nil. func (t *Form) SetModified(v map[string]bool) *Form { t.Modified = 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: "handleKeyNav", Doc: "handleKeyNav indicates whether this frame should handle keyboard\nnavigation events using the default handlers. Set to false to allow\ncustom event handling."}, {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: "prevColor", Doc: "prevColor is the previously rendered color, as uniform."}, {Name: "prevOpacity", Doc: "prevOpacity is the previously rendered opacity."}, {Name: "pixels", Doc: "image representation of the icon, cached for faster drawing."}}}) // 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 own [paint.Painter]. The [Scene] is set in a\n[Stage], which the [Scene] has a pointer to.\n\nEach [Scene] contains state specific to its particular usage\nwithin a given [Stage] and overall rendering context, representing the unit\nof rendering in the Cogent Core framework.", Directives: []types.Directive{{Tool: "core", Directive: "no-new"}}, Methods: []types.Method{{Name: "standardContextMenu", Doc: "standardContextMenu adds standard context menu items for the [Scene].", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"m"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Body", Doc: "Body provides the main contents of scenes that use control Bars\nto allow the main window contents to be specified separately\nfrom that dynamic control content. When constructing scenes using\na [Body], you can operate directly on the [Body], which has wrappers\nfor most major Scene functions."}, {Name: "WidgetInit", Doc: "WidgetInit is a function called on every newly created [Widget].\nThis can be used to set global configuration and styling for all\nwidgets in conjunction with [App.SceneInit]."}, {Name: "Bars", Doc: "Bars are functions for creating control bars,\nattached to different sides of a [Scene]. Functions\nare called in forward order so first added are called first."}, {Name: "Data", Doc: "Data is the optional data value being represented by this scene.\nUsed e.g., for recycling views of a given item instead of creating new one."}, {Name: "SceneGeom", Doc: "Size and position relative to overall rendering context."}, {Name: "Painter", Doc: "painter for rendering"}, {Name: "Events", Doc: "event manager for this scene"}, {Name: "Stage", Doc: "current stage in which this Scene is set"}, {Name: "Animations", Doc: "Animations are the currently active [Animation]s in this scene."}, {Name: "renderBBoxes", Doc: "renderBBoxes indicates to render colored bounding boxes for all of the widgets\nin the scene. This is enabled by the [Inspector] in select element mode."}, {Name: "renderBBoxHue", Doc: "renderBBoxHue is current hue for rendering bounding box in [Scene.RenderBBoxes] mode."}, {Name: "selectedWidget", Doc: "selectedWidget is the currently selected/hovered widget through the [Inspector] selection mode\nthat should be highlighted with a background color."}, {Name: "selectedWidgetChan", Doc: "selectedWidgetChan is the channel on which the selected widget through the inspect editor\nselection mode is transmitted to the inspect editor after the user is done selecting."}, {Name: "renderer", Doc: "source renderer for rendering the scene"}, {Name: "lastRender", Doc: "lastRender captures key params from last render.\nIf different then a new ApplyStyleScene is needed."}, {Name: "showIter", Doc: "showIter counts up at start of showing a Scene\nto trigger Show event and other steps at start of first show"}, {Name: "directRenders", Doc: "directRenders are widgets that render directly to the [RenderWindow]\ninstead of rendering into the Scene Painter."}, {Name: "flags", Doc: "flags are atomic bit flags for [Scene] state."}}}) // 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: "DocsFontSize", Doc: "Font size factor applied only to documentation and other\ndense text contexts, not normal interactive elements.\nIt is a percentage of the base Font size setting (higher numbers\nlead to larger text)."}, {Name: "ZebraStripes", Doc: "the amount that alternating rows are highlighted when showing\ntabular data (set to 0 to disable zebra striping)."}, {Name: "Screens", Doc: "screen-specific settings, which will override overall defaults if set,\nso different screens can use different zoom levels.\nUse 'Save screen zoom' in the toolbar to save the current zoom for the current\nscreen, and Control +/- keyboard shortcut to change this zoom level anytime."}, {Name: "Highlighting", Doc: "text highlighting style / theme."}, {Name: "Text", Doc: "Text specifies text settings including the language, and the\nfont families for different styles of fonts."}}}) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.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;\nif this is set to true, pressing the close button on other tabs\nwill take you to that tab, from which you can close it."}, {Name: "BigFileSize", Doc: "the limit of file size, above which user will be prompted before\nopening / copying, etc."}, {Name: "SavedPathsMax", Doc: "maximum number of saved paths to save in FilePicker"}, {Name: "User", Doc: "user info, which is partially filled-out automatically if empty\nwhen settings are first created."}, {Name: "FavPaths", Doc: "favorite paths, shown in FilePickerer and also editable there"}, {Name: "FilePickerSort", Doc: "column to sort by in FilePicker, and :up or :down for direction.\nUpdated automatically via FilePicker"}, {Name: "MenuMaxHeight", Doc: "the maximum height of any menu popup panel in units of font height;\nscroll bars are enforced beyond that size."}, {Name: "CompleteWaitDuration", Doc: "the amount of time to wait before offering completions"}, {Name: "CompleteMaxItems", Doc: "the maximum number of completions offered in popup"}, {Name: "CursorBlinkTime", Doc: "time interval for cursor blinking on and off -- set to 0 to disable blinking"}, {Name: "LayoutAutoScrollDelay", Doc: "The amount of time to wait before trying to autoscroll again"}, {Name: "LayoutPageSteps", Doc: "number of steps to take in PageUp / Down events in terms of number of items"}, {Name: "LayoutFocusNameTimeout", Doc: "the amount of time between keypresses to combine characters into name\nto search for within layout -- starts over after this delay."}, {Name: "LayoutFocusNameTabTime", Doc: "the amount of time since last focus name event to allow tab to focus\non next element with same name."}, {Name: "MapInlineLength", Doc: "the number of map elements at or below which an inline representation\nof the map will be presented, which is more convenient for small #'s of properties"}, {Name: "StructInlineLength", Doc: "the number of elemental struct fields at or below which an inline representation\nof the struct will be presented, which is more convenient for small structs"}, {Name: "SliceInlineLength", Doc: "the number of slice elements below which inline will be used"}}}) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.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.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.\nFor dialogs, this position is the target center position, not the upper-left corner."}, {Name: "Main", Doc: "If a popup stage, this is the main stage that owns it (via its [Stage.popups]).\nIf a main stage, it points to itself."}, {Name: "popups", Doc: "For main stages, this is the stack of the popups within it\n(created specifically for the main stage).\nFor popups, this is the pointer to the popups within the\nmain stage managing it."}, {Name: "Mains", Doc: "For all stages, this is the main [Stages] that lives in a [renderWindow]\nand manages the main stages."}, {Name: "renderContext", Doc: "rendering context which has info about the RenderWindow onto which we render.\nThis should be used instead of the RenderWindow itself for all relevant\nrendering information. This is only available once a Stage is Run,\nand must always be checked for nil."}, {Name: "Sprites", Doc: "Sprites are named images that are rendered last overlaying everything else."}}}) // 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. // For dialogs, this position is the target center position, not the upper-left corner. 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: "SaveImage", Doc: "SaveImage saves the current rendered SVG image to an image file,\nusing the filename extension to determine the file type.", 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: "renderer", Doc: "image renderer"}, {Name: "image", Doc: "cached rendered image"}, {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].", Methods: []types.Method{{Name: "copy", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, 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: "Links", Doc: "Links is the list of links in the text."}, {Name: "richText", Doc: "richText is the conversion of the HTML text source."}, {Name: "paintText", Doc: "paintText is the [shaped.Lines] for the text."}, {Name: "normalCursor", Doc: "normalCursor is the cached cursor to display when there\nis no link being hovered."}, {Name: "selectRange", Doc: "selectRange is the selected range, in _runes_, which must be applied"}}}) // 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: "complete", Doc: "complete contains functions and data for text field completion.\nIt must be set using [TextField.SetCompleter]."}, {Name: "text", Doc: "text is the last saved value of the text string being edited."}, {Name: "edited", Doc: "edited is whether the text has been edited relative to the original."}, {Name: "editText", Doc: "editText is the live text string being edited, with the latest modifications."}, {Name: "error", Doc: "error is the current validation error of the text field."}, {Name: "effPos", Doc: "effPos is the effective position with any leading icon space added."}, {Name: "effSize", Doc: "effSize is the effective size, subtracting any leading and trailing icon space."}, {Name: "dispRange", Doc: "dispRange is the range of visible text, for scrolling text case (non-wordwrap)."}, {Name: "cursorPos", Doc: "cursorPos is the current cursor position as rune index into string."}, {Name: "cursorLine", Doc: "cursorLine is the current cursor line position, for word wrap case."}, {Name: "charWidth", Doc: "charWidth is the approximate number of chars that can be\ndisplayed at any time, which is computed from the font size."}, {Name: "selectRange", Doc: "selectRange is the selected range."}, {Name: "selectInit", Doc: "selectInit is the initial selection position (where it started)."}, {Name: "selectMode", Doc: "selectMode is whether to select text as the cursor moves."}, {Name: "selectModeShift", Doc: "selectModeShift is whether selectmode was turned on because of the shift key."}, {Name: "renderAll", Doc: "renderAll is the render version of entire text, for sizing."}, {Name: "renderVisible", Doc: "renderVisible is the render version of just the visible text in dispRange."}, {Name: "renderedRange", Doc: "renderedRange is the dispRange last rendered."}, {Name: "numLines", Doc: "number of lines from last render update, for word-wrap version"}, {Name: "lineHeight", Doc: "lineHeight is the line height cached during styling."}, {Name: "blinkOn", Doc: "blinkOn oscillates between on and off for blinking."}, {Name: "cursorMu", Doc: "cursorMu is the mutex for updating the cursor between blinker and field."}, {Name: "undos", Doc: "undos is the undo manager for the text field."}, {Name: "leadingIconButton"}, {Name: "trailingIconButton"}}}) // 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 } 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]. All toolbars use the\n[WidgetBase.Maker] system, so you cannot directly add widgets; see\nhttps://cogentcore.org/core/toolbar.", 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]. All toolbars use the // [WidgetBase.Maker] system, so you cannot directly add widgets; see // https://cogentcore.org/core/toolbar. func NewToolbar(parent ...tree.Node) *Toolbar { return tree.New[Toolbar](parent...) } var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Treer", IDName: "treer", Doc: "Treer is an interface for [Tree] types\nproviding access to the base [Tree] and\noverridable method hooks for actions taken on the [Tree],\nincluding OnOpen, OnClose, etc.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "AsCoreTree", Doc: "AsTree returns the base [Tree] for this node.", Returns: []string{"Tree"}}, {Name: "CanOpen", Doc: "CanOpen returns true if the node is able to open.\nBy default it checks HasChildren(), but could check other properties\nto perform lazy building of the tree.", Returns: []string{"bool"}}, {Name: "OnOpen", Doc: "OnOpen is called when a node is toggled open.\nThe base version does nothing."}, {Name: "OnClose", Doc: "OnClose is called when a node is toggled closed.\nThe base version does nothing."}, {Name: "MimeData", Args: []string{"md"}}, {Name: "Cut"}, {Name: "Copy"}, {Name: "Paste"}, {Name: "DragDrop", Args: []string{"e"}}, {Name: "DropDeleteSource", Args: []string{"e"}}}}) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Tree", IDName: "tree", Doc: "Tree provides a graphical representation of a tree structure,\nproviding full navigation and manipulation abilities.\n\nIt does not handle layout by itself, so if you want it to scroll\nseparately from the rest of the surrounding context, you must\nplace it in a [Frame].\n\nIf the [Tree.SyncNode] field is non-nil, typically via the\n[Tree.SyncTree] method, then the Tree mirrors another\ntree structure, and tree editing functions apply to\nthe source tree first, and then to the Tree by sync.\n\nOtherwise, data can be directly encoded in a Tree\nderived type, to represent any kind of tree structure\nand associated data.\n\nStandard [events.Event]s are sent to any listeners, including\n[events.Select], [events.Change], and [events.DoubleClick].\nThe selected nodes are in the root [Tree.SelectedNodes] list;\nselect events are sent to both selected nodes and the root node.\nSee [Tree.IsRootSelected] to check whether a select event on the root\nnode corresponds to the root node or another node.", Methods: []types.Method{{Name: "OpenAll", Doc: "OpenAll opens the node and all of its sub-nodes.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "CloseAll", Doc: "CloseAll closes the node and all of its sub-nodes.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Copy", Doc: "Copy copies the tree to the clipboard.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Cut", Doc: "Cut copies to [system.Clipboard] and deletes selected items.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Paste", Doc: "Paste pastes clipboard at given node.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "InsertAfter", Doc: "InsertAfter inserts a new node in the tree\nafter this node, at the same (sibling) level,\nprompting for the type of node to insert.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "InsertBefore", Doc: "InsertBefore inserts a new node in the tree\nbefore this node, at the same (sibling) level,\nprompting for the type of node to insert\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "AddChildNode", Doc: "AddChildNode adds a new child node to this one in the tree,\nprompting the user for the type of node to add\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "DeleteNode", Doc: "DeleteNode deletes the tree node or sync node corresponding\nto this view node in the sync tree.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Duplicate", Doc: "Duplicate duplicates the sync node corresponding to this view node in\nthe tree, and inserts the duplicate after this node (as a new sibling).\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "editNode", Doc: "editNode pulls up a [Form] dialog for the node.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "inspectNode", Doc: "inspectNode pulls up a new Inspector window on the node.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "SyncNode", Doc: "SyncNode, if non-nil, is the [tree.Node] that this widget is\nviewing in the tree (the source). It should be set using\n[Tree.SyncTree]."}, {Name: "Text", Doc: "Text is the text to display for the tree item label, which automatically\ndefaults to the [tree.Node.Name] of the tree node. It has no effect\nif [Tree.SyncNode] is non-nil."}, {Name: "Icon", Doc: "Icon is an optional icon displayed to the the left of the text label."}, {Name: "IconOpen", Doc: "IconOpen is the icon to use for an open (expanded) branch;\nit defaults to [icons.KeyboardArrowDown]."}, {Name: "IconClosed", Doc: "IconClosed is the icon to use for a closed (collapsed) branch;\nit defaults to [icons.KeyboardArrowRight]."}, {Name: "IconLeaf", Doc: "IconLeaf is the icon to use for a terminal node branch that has no children;\nit defaults to [icons.Blank]."}, {Name: "TreeInit", Doc: "TreeInit is a function that can be set on the root node that is called\nwith each child tree node when it is initialized. It is only\ncalled with the root node itself in [Tree.SetTreeInit], so you\nshould typically call that instead of setting this directly."}, {Name: "Indent", Doc: "Indent is the amount to indent children relative to this node.\nIt should be set in a Styler like all other style properties."}, {Name: "OpenDepth", Doc: "OpenDepth is the depth for nodes be initialized as open (default 4).\nNodes beyond this depth will be initialized as closed."}, {Name: "Closed", Doc: "Closed is whether this tree node is currently toggled closed\n(children not visible)."}, {Name: "SelectMode", Doc: "SelectMode, when set on the root node, determines whether keyboard movements should update selection."}, {Name: "viewIndex", Doc: "linear index of this node within the entire tree.\nupdated on full rebuilds and may sometimes be off,\nbut close enough for expected uses"}, {Name: "widgetSize", Doc: "size of just this node widget.\nour alloc includes all of our children, but we only draw us."}, {Name: "Root", Doc: "Root is the cached root of the tree. It is automatically set."}, {Name: "SelectedNodes", Doc: "SelectedNodes holds the currently selected nodes.\nIt is only set on the root node. See [Tree.GetSelectedNodes]\nfor a version that also works on non-root nodes."}, {Name: "actStateLayer", Doc: "actStateLayer is the actual state layer of the tree, which\nshould be used when rendering it and its parts (but not its children).\nthe reason that it exists is so that the children of the tree\n(other trees) do not inherit its stateful background color, as\nthat does not look good."}, {Name: "inOpen", Doc: "inOpen is set in the Open method to prevent recursive opening for lazy-open nodes."}, {Name: "Branch", Doc: "Branch is the branch widget that is used to open and close the tree node."}}}) // 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; // select events are sent to both selected nodes and the root node. // See [Tree.IsRootSelected] to check whether a select event on the root // node corresponds to the root node or another node. 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.HighlightingButton", IDName: "highlighting-button", Doc: "HighlightingButton represents a [HighlightingName] with a button.", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "HighlightingName"}}}) // NewHighlightingButton returns a new [HighlightingButton] with the given optional parent: // HighlightingButton represents a [HighlightingName] with a button. func NewHighlightingButton(parent ...tree.Node) *HighlightingButton { return tree.New[HighlightingButton](parent...) } // SetHighlightingName sets the [HighlightingButton.HighlightingName] func (t *HighlightingButton) SetHighlightingName(v string) *HighlightingButton { t.HighlightingName = v return t } 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) 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) } return NewFormButton() case kind == reflect.Map: len := uv.Len() if !noInline && (inline || len <= SystemSettings.MapInlineLength) { return NewKeyedList().SetInline(true) } 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() } 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[FontName, TextField]() AddValueType[keymap.MapName, KeyMapButton]() AddValueType[key.Chord, KeyChordButton]() AddValueType[HighlightingName, HighlightingButton]() } // 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/styles" "cogentcore.org/core/text/fonts" "cogentcore.org/core/text/highlighting" "cogentcore.org/core/text/rich" "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 !reflectx.UnderlyingPointer(reflect.ValueOf(tb.Tree)).IsNil() { path = tb.Tree.AsTree().String() } tb.SetText(path) }) InitValueButton(tb, true, func(d *Body) { if !reflectx.UnderlyingPointer(reflect.ValueOf(tb.Tree)).IsNil() { makeInspector(d, tb.Tree) } }) } func (tb *TreeButton) WidgetTooltip(pos image.Point) (string, image.Point) { if reflectx.UnderlyingPointer(reflect.ValueOf(tb.Tree)).IsNil() { 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 = rich.FontName // 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) fb.Updater(func() { if fb.Text == "" { fb.SetText("(default)") } }) InitValueButton(fb, false, func(d *Body) { d.SetTitle("Select a font family") si := 0 fi := fonts.Families(fb.Scene.TextShaper().FontList()) tb := NewTable(d) tb.SetSlice(&fi).SetSelectedField("Family").SetSelectedValue(fb.Text).BindSelect(&si) tb.SetTableStyler(func(w Widget, s *styles.Style, row, col int) { if col != 1 { return } s.Font.CustomFont = rich.FontName(fi[row].Family) s.Font.Family = rich.Custom s.Font.Size.Dp(24) }) tb.OnChange(func(e events.Event) { fb.Text = fi[si].Family }) }) } // HighlightingName is a highlighting style name. type HighlightingName = highlighting.HighlightingName // HighlightingButton represents a [HighlightingName] with a button. type HighlightingButton struct { Button HighlightingName string } func (hb *HighlightingButton) WidgetValue() any { return &hb.HighlightingName } func (hb *HighlightingButton) Init() { hb.Button.Init() hb.SetType(ButtonTonal).SetIcon(icons.Brush) hb.Updater(func() { hb.SetText(hb.HighlightingName) }) InitValueButton(hb, false, func(d *Body) { d.SetTitle("Select a syntax highlighting style") si := 0 ls := NewList(d).SetSlice(&highlighting.StyleNames).SetSelectedValue(hb.HighlightingName).BindSelect(&si) ls.OnChange(func(e events.Event) { hb.HighlightingName = highlighting.StyleNames[si] }) }) } // Editor opens an editor of highlighting styles. func HighlightingEditor(st *highlighting.Styles) { if RecycleMainWindow(st) { return } d := NewBody("Highlighting styles").SetData(st) NewText(d).SetType(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 := NewKeyedList(d).SetMap(st) highlighting.StylesChanged = false kl.OnChange(func(e events.Event) { highlighting.StylesChanged = true }) d.AddTopBar(func(bar *Frame) { NewToolbar(bar).Maker(func(p *tree.Plan) { tree.Add(p, func(w *FuncButton) { w.SetFunc(st.OpenJSON).SetText("Open from file").SetIcon(icons.Open) w.Args[0].SetTag(`extension:".highlighting"`) }) tree.Add(p, func(w *FuncButton) { w.SetFunc(st.SaveJSON).SetText("Save from file").SetIcon(icons.Save) w.Args[0].SetTag(`extension:".highlighting"`) }) tree.Add(p, func(w *Button) { w.SetText("View standard").SetIcon(icons.Visibility).OnClick(func(e events.Event) { HighlightingEditor(&highlighting.StandardStyles) }) }) tree.Add(p, func(w *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 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/composer" "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 the 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 // RenderSource returns the self-contained [composer.Source] for // rendering this widget. The base widget returns nil, and the [Scene] // widget returns the [paint.Painter] rendering results. // Widgets that do direct rendering instead of drawing onto // the Scene painter should return a suitable render source. // Use [Scene.AddDirectRender] to register such widgets with the Scene. // The given draw operation is the suggested way to Draw onto existing images. RenderSource(op draw.Op) composer.Source } // 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 ) // 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.handleWidgetStateFromAttend() 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] } // IsDisplayable returns whether the widget has the potential of being displayed. // If it or any of its parents are deleted or [states.Invisible], it is not // displayable. Otherwise, it is displayable. // // This does *not* check if the widget is actually currently visible, for which you // can use [WidgetBase.IsVisible]. In other words, if a widget is currently offscreen // but can be scrolled onscreen, it is still displayable, but it is not visible until // its bounding box is actually onscreen. // // Widgets that are not displayable are automatically not rendered and do not get // window events. // // [styles.DisplayNone] can be set for [styles.Style.Display] to make a widget // not displayable. func (wb *WidgetBase) IsDisplayable() 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().IsDisplayable() } // IsVisible returns whether the widget is actually currently visible. // A widget is visible if and only if it is both [WidgetBase.IsDisplayable] // and it has a non-empty rendering bounding box (ie: it is currently onscreen). // This means that widgets currently not visible due to scrolling will return false // for this function, even though they are still displayable and return true for // [WidgetBase.IsDisplayable]. func (wb *WidgetBase) IsVisible() bool { return wb.IsDisplayable() && !wb.Geom.TotalBBox.Empty() } // RenderSource returns the self-contained [composer.Source] for // rendering this widget. The base widget returns nil, and the [Scene] // widget returns the [paint.Painter] rendering results. // Widgets that do direct rendering instead of drawing onto // the Scene painter should return a suitable render source. // Use [Scene.AddDirectRender] to register such widgets with the Scene. // The given draw operation is the suggested way to Draw onto existing images. func (wb *WidgetBase) RenderSource(op draw.Op) composer.Source { return nil } // 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 { if tree.IsNil(c) { continue } 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, c := range wb.Children { if tree.IsNil(c) { continue } w, cwb := c.(Widget), AsWidget(c) 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 && !(e.Type() == events.Attend || e.Type() == events.AttendLost) { 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.IsReadOnly() { 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) } }) } // 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) { if !TheApp.SystemPlatform().IsMobile() { return } 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 !TheApp.SystemPlatform().IsMobile() { return } 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() } } }) } // handleWidgetStateFromAttend updates standard State flags based on Attend events func (wb *WidgetBase) handleWidgetStateFromAttend() { wb.On(events.Attend, func(e events.Event) { if wb.Styles.Abilities.IsPressable() { wb.SetState(true, states.Attended) } }) wb.On(events.AttendLost, func(e events.Event) { if wb.Styles.Abilities.IsPressable() { wb.SetState(false, states.Attended) } }) } // 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() } }) } // SendChangeOnInput adds an event handler that does [WidgetBase.SendChange] // in [WidgetBase.OnInput]. This is not done by default, but you can call it // if you want [events.Input] to trigger full change events, such as in a [Bind] // context. func (wb *WidgetBase) SendChangeOnInput() { wb.OnInput(func(e events.Event) { wb.SendChange(e) }) } // SendClickOnEnter 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) SendClickOnEnter() { wb.OnKeyChord(func(e events.Event) { kf := keymap.Of(e.KeyChord()) if DebugSettings.KeyEventTrace { slog.Info("WidgetBase.SendClickOnEnter", "widget", wb, "keyFunction", kf) } if kf == keymap.Accept { wb.Send(events.Click, e) // don't SetHandled } else if kf == keymap.Enter || e.KeyRune() == ' ' { e.SetHandled() wb.Send(events.Click, e) } }) } // dragStateReset resets the drag related state flags, including [states.Active]. func (wb *WidgetBase) dragStateReset() { wb.SetState(false, states.Active, states.DragHovered, states.Dragging) } //////// 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]. // // SetFocus only fully works for widgets that have already been shown, so for newly // created widgets, you should use [WidgetBase.StartFocus], or [WidgetBase.Defer] your // SetFocus call. 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 } // SetAttend sends [events.Attend] to this widget if it is pressable. func (wb *WidgetBase) SetAttend() { if !wb.Styles.Abilities.IsPressable() { return } em := wb.Events() if em != nil { em.setAttend(wb.This.(Widget)) } } // 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 or title // 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 || w.title == 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.flags.HasFlag(winGotFocus) { 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/math32" "cogentcore.org/core/paint/pimage" "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(math32.Vec2(float32(size), float32(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) } img := sv.RenderImage() blurRadius := size / 16 bounds := img.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{}, img, image.Point{}, draw.Src) shadow = pimage.GaussianBlur(shadow, float64(blurRadius)) draw.Draw(shadow, shadow.Bounds(), img, 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/content" "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/htmlcore" "cogentcore.org/core/icons" "cogentcore.org/core/math32" "cogentcore.org/core/styles" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" "cogentcore.org/core/text/textcore" "cogentcore.org/core/tree" "cogentcore.org/core/yaegicore" "cogentcore.org/core/yaegicore/coresymbols" ) //go:embed content var econtent 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") ct := content.NewContent(b).SetContent(econtent) ctx := ct.Context ctx.AddWikilinkHandler(htmlcore.GoDocWikilink("doc", "cogentcore.org/core")) b.AddTopBar(func(bar *core.Frame) { tb := core.NewToolbar(bar) tb.Maker(ct.MakeToolbar) tb.Maker(func(p *tree.Plan) { tree.Add(p, func(w *core.Button) { ctx.LinkButton(w, "playground") w.SetText("Playground").SetIcon(icons.PlayCircle) }) tree.Add(p, func(w *core.Button) { ctx.LinkButton(w, "https://youtube.com/@CogentCore") w.SetText("Videos").SetIcon(icons.VideoLibrary) }) tree.Add(p, func(w *core.Button) { ctx.LinkButton(w, "https://cogentcore.org/blog") w.SetText("Blog").SetIcon(icons.RssFeed) }) tree.Add(p, func(w *core.Button) { ctx.LinkButton(w, "https://github.com/cogentcore/core") w.SetText("GitHub").SetIcon(icons.GitHub) }) tree.Add(p, func(w *core.Button) { ctx.LinkButton(w, "https://cogentcore.org/community") w.SetText("Community").SetIcon(icons.Forum) }) tree.Add(p, func(w *core.Button) { ctx.LinkButton(w, "https://github.com/sponsors/cogentcore") w.SetText("Sponsor").SetIcon(icons.Favorite) }) }) }) coresymbols.Symbols["."]["econtent"] = reflect.ValueOf(econtent) coresymbols.Symbols["."]["myImage"] = reflect.ValueOf(myImage) coresymbols.Symbols["."]["mySVG"] = reflect.ValueOf(mySVG) coresymbols.Symbols["."]["myFile"] = reflect.ValueOf(myFile) ctx.ElementHandlers["home-page"] = homePage ctx.ElementHandlers["core-playground"] = func(ctx *htmlcore.Context) bool { splits := core.NewSplits(ctx.BlockParent) ed := textcore.NewEditor(splits) playgroundFile := filepath.Join(core.TheApp.AppDataDir(), "playground.go") err := ed.Lines.Open(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.Lines.Open(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.SaveQuiet(), "Error saving code") }) parent := core.NewFrame(splits) yaegicore.BindTextEditor(ed, parent, "Go") return true } ctx.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, txt 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 = text.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 = rich.Bold s.Color = colors.Scheme.Primary.Base }) }) tree.AddChild(w, func(w *core.Text) { w.SetType(core.TextTitleLarge).SetText(txt) }) }) 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) { ctx.LinkButton(w, "basics") w.SetText("Get started") }) tree.AddChild(w, func(w *core.Button) { ctx.LinkButton(w, "install") w.SetText("Install").SetType(core.ButtonTonal) }) }) 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 run on macOS, Windows, Linux, iOS, Android, and the web, automatically scaling to any screen. Instead of struggling with platform-specific code in multiple languages, you can 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 type safety and a robust design that doesn't get in your way. Cogent Core makes it easy to get started with cross-platform app development in just two commands and three lines of simple code.", func(w *textcore.Editor) { w.Lines.SetLanguage(fileinfo.Go).SetString(`b := core.NewBody() core.NewButton(b).SetText("Hello, World!") b.RunMainWindow()`) w.SetReadOnly(true) w.Lines.Settings.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 customize apps to fit their needs and preferences through a robust styling system and powerful color settings.", func(w *core.Form) { w.SetStruct(core.AppearanceSettings) w.OnChange(func(e events.Event) { core.UpdateSettings(w, core.AppearanceSettings) }) }) makeBlock("POWERFUL FEATURES", "Cogent Core supports text editors, video 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 data binding and app inspection.", func(w *core.Icon) { initIcon(w).SetIcon(icons.ScatterPlot) }) makeBlock("OPTIMIZED EXPERIENCE", "Cogent Core has editable, interactive, example-based documentation, video tutorials, command line tools, and support from the 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 at high speeds. Apps compile to machine code, allowing them to run without 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.", func(w *core.Icon) { initIcon(w).SetIcon(icons.Code) }) makeBlock("USED AROUND THE WORLD", "Over seven years of development, Cogent Core has been used and tested by developers and scientists around the world for various use cases. Cogent Core is an advanced framework 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 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 vector graphics editor with 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 LAB", "Cogent Lab is an extensible math, data science, and statistics platform and language.", func(w *core.SVG) { errors.Log(w.OpenFS(resources, "numbers-icon.svg")) }, "https://cogentcore.org/lab") makeBlock("COGENT MAIL", "Cogent Mail is a customizable email client with built-in Markdown support, automatic mail filtering, and keyboard shortcuts for 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 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 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 simplify your development experience.", func(w *core.Icon) { initIcon(w).SetIcon(icons.Check) }) tree.AddChild(home, func(w *core.Button) { ctx.LinkButton(w, "basics") w.SetText("Get started") }) 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" "cogentcore.org/core/base/logx" "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 { logx.PrintlnInfo(err) return err } err = GeneratePkgs(cfg, pkgs) logx.PrintlnInfo(err) return err } // 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, "enumgen.go", "typegen.go") } // 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 generate.ExcludeFile(g.Pkg, file, "enumgen.go", "typegen.go") { 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 NConstantTmplGosl = template.Must(template.New("StringNConstant").Parse( `//gosl:start //{{.Name}}N is the highest valid value for type {{.Name}}, plus one. const {{.Name}}N {{.Name}} = {{.MaxValueP1}} //gosl:end `)) 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 if g.Config.Gosl { g.ExecTmpl(NConstantTmplGosl, typ) } else { 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 this bit flag value has the given bit flag set. func HasFlag(i *int64, f BitFlag) bool { return atomic.LoadInt64(i)&(1<<uint32(f.Int64())) != 0 } // HasAnyFlags returns whether this bit flag value has any of the given bit flags set. func HasAnyFlags(i *int64, f ...BitFlag) bool { var mask int64 for _, v := range f { mask |= 1 << v.Int64() } return atomic.LoadInt64(i)&mask != 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, 46, 47} // TypesN is the highest valid value for type Types, plus one. const TypesN Types = 48 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, `Attend`: 36, `AttendLost`: 37, `Change`: 38, `Input`: 39, `Show`: 40, `Close`: 41, `Window`: 42, `WindowResize`: 43, `WindowPaint`: 44, `OS`: 45, `OSOpenFiles`: 46, `Custom`: 47} 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. The [MouseScroll.Delta] on scroll events is always in real pixel/dot units; low-level sources may be in lines or pages, but we normalize everything to real pixels/dots.`, 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: `Attend is sent when a Pressable element is programmatically set as Attended through an event. Typically the Attended state is engaged by clicking. Attention is like Focus, in that there is only 1 element at a time in the Attended state, but it does not direct keyboard input. The primary effect of attention is on scrolling events via [abilities.ScrollableUnattended].`, 37: `AttendLost is sent when a different Pressable element is Attended.`, 38: `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.`, 39: `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.`, 40: `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".`, 41: `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.`, 42: `Window reports on changes in the window position, visibility (iconify), focus changes, screen update, and closing. These are only sent once per event (Unique).`, 43: `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.`, 44: `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.`, 45: `OS is an operating system generated event (app level typically)`, 46: `OSOpenFiles is an event telling app to open given files`, 47: `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: `Attend`, 37: `AttendLost`, 38: `Change`, 39: `Input`, 40: `Show`, 41: `Close`, 42: `Window`, 43: `WindowResize`, 44: `WindowPaint`, 45: `OS`, 46: `OSOpenFiles`, 47: `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! } 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, which is always in pixel/dot // units (see [Scroll]). 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) } // WindowPaint sends a [NewWindowPaint] event. 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. // The [MouseScroll.Delta] on scroll events is always in real pixel/dot units; // low-level sources may be in lines or pages, but we normalize everything // to real pixels/dots. 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 // Attend is sent when a Pressable element is programmatically set // as Attended through an event. Typically the Attended state is engaged // by clicking. Attention is like Focus, in that there is only 1 element // at a time in the Attended state, but it does not direct keyboard input. // The primary effect of attention is on scrolling events via // [abilities.ScrollableUnattended]. Attend // AttendLost is sent when a different Pressable element is Attended. AttendLost // 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/text/textcore" "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) } core.NewText(tab).SetText("Emojis: 🧁🍰🎁") core.NewText(tab).SetText("Hebrew/RTL: אָהַבְתָּ אֵת יְיָ | אֱלֹהֶיךָ, בְּכָל-לְבָֽבְךָ, Let there be light וּבְכָל-נַפְשְׁךָ,") core.NewText(tab).SetText("Chinese/Japanese/Korean: 国際化活動・計算機科学を勉強する・한국어") } 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) ed := textcore.NewEditor(sp) ed.Styler(func(s *styles.Style) { s.Padding.Set(units.Em(core.ConstantSpacing(1))) }) ed.Lines.OpenFS(demoFile, "demo.go") textcore.NewEditor(sp).Lines.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.Text.SansSerif 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 // this is now showing ShowMe string // click to show next On bool // a condition Condition int `default:"0"` // if On && Condition == 0 Condition1 string // if On && Condition <= 1 Condition2 tableStruct // a value Value float32 `default:"1"` } 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) 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/text/textcore" ) // 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) { textcore.DiffFiles(fn, tpath, srcpath) }) d.AddOK(bar).SetText("Overwrite").OnClick(func(e events.Event) { fsx.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) { textcore.DiffFiles(fn, tpath, srcpath) }) d.AddOK(bar).SetText("Diff to existing").OnClick(func(e events.Event) { npath := filepath.Join(string(tdir.Filepath), fname) textcore.DiffFiles(fn, npath, srcpath) }) d.AddOK(bar).SetText("Overwrite target").OnClick(func(e events.Event) { fsx.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) fsx.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) { textcore.DiffFiles(fn, tpath, srcpath) }) d.AddOK(bar).SetText("Overwrite target").OnClick(func(e events.Event) { fsx.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 ( "os" "path/filepath" "slices" "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 open directories and sorting preferences. // The strings are typically relative paths. Map access is protected by Mutex. type DirFlagMap struct { // map of paths and associated flags Map map[string]dirFlags // mutex for accessing map sync.Mutex } // init initializes the map, and sets the Mutex lock; must unlock manually. func (dm *DirFlagMap) init() { dm.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.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.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.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.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.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 } // openPaths returns a list of open paths func (dm *DirFlagMap) openPaths(root string) []string { dm.init() defer dm.Unlock() var paths []string for fn, df := range dm.Map { if !df.HasFlag(dirIsOpen) { continue } fpath := filepath.Join(root, fn) _, err := os.Stat(fpath) if err != nil { delete(dm.Map, fn) continue } rootClosed := false par := fn for { par = filepath.Dir(par) if par == "" || par == "." { break } if pdf, ook := dm.Map[par]; ook { if !pdf.HasFlag(dirIsOpen) { rootClosed = true break } } } if rootClosed { continue } paths = append(paths, fpath) } slices.Sort(paths) return paths } // 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 overrides SortByModTime default on Tree if set.`, 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") } // 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/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. // This allows apps to intervene and apply any additional logic for these actions. type Filer interface { //types:add core.Treer // AsFileNode returns the [Node] AsFileNode() *Node // RenameFiles renames any selected files. RenameFiles() // DeleteFiles deletes any selected files. DeleteFiles() // 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) // SelectedPaths returns the paths of selected nodes. func (fn *Node) SelectedPaths() []string { sels := fn.GetSelectedNodes() n := len(sels) if n == 0 { return nil } paths := make([]string, n) fn.SelectedFunc(func(sn *Node) { paths = append(paths, string(sn.Filepath)) }) return paths } // 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 } // DeleteFiles 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.DeleteFilesNoPrompts() }) }) d.RunDialog(fn) } // DeleteFilesNoPrompts does the actual deletion, no prompts. func (fn *Node) DeleteFilesNoPrompts() { fn.FileRoot().NeedsLayout() fn.SelectedFunc(func(sn *Node) { if !sn.Info.IsDir() { sn.DeleteFile() return } 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) } 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 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 { fn.FileRoot().UpdatePath(ppath) nfn, ok := fn.FileRoot().FindFile(np) if ok && !nfn.IsRoot() && string(nfn.Filepath) == np { core.MessageSnackbar(fn, "Adding new file to VCS: "+fsx.DirAndFile(string(nfn.Filepath))) nfn.AddToVCS() } } fn.FileRoot().UpdatePath(ppath) } // 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) fsx.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.IsRoot() { 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().RunWindowDialog(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.This.(Filer).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) fb := core.NewFuncButton(m).SetFunc(fn.newFiles) fb.SetText("New file").SetIcon(icons.OpenInNew).SetEnabled(fn.HasSelection()) fb.Args[1].SetValue(true) // todo: not working 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/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/text/highlighting" "cogentcore.org/core/text/rich" "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:"-"` // FileIsOpen indicates that this file has been opened, indicated by Italics. FileIsOpen bool // 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:"-"` } func (fn *Node) AsFileNode() *Node { return fn } // FileRoot returns the Root node as a [Tree]. func (fn *Node) FileRoot() *Tree { return AsTree(fn.Root) } 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) { fn.styleFromStatus() }) 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.This.(Filer).DeleteFiles() e.SetHandled() case keymap.Backspace: fn.This.(Filer).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 = rich.Bold } if fn.FileIsOpen { s.Font.Slant = rich.Italic } }) }) fn.Updater(func() { fn.setFileIcon() if fn.IsDir() { repo, rnode := fn.Repo() if repo != nil && rnode.This == fn.This { rnode.updateRepoFiles() } } else { fn.This.(Filer).GetFileInfo() } fn.Text = fn.Info.Name cc := fn.Styles.Color fn.styleFromStatus() if fn.Styles.Color != cc && fn.Parts != nil { fn.Parts.StyleTree() } }) 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.Root = fn.Root w.NeedsLayout() 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.Root = fn.Root w.NeedsLayout() w.Filepath = core.Filename(fpath) w.This.(Filer).GetFileInfo() if w.FileRoot().FS == nil { if w.IsDir() && repo == nil { w.detectVCSRepo() } } }) } }) } // styleFromStatus updates font color from func (fn *Node) styleFromStatus() { status := fn.Info.VCS hex := "" switch { case status == vcs.Untracked: hex = "#808080" case status == vcs.Modified: hex = "#4b7fd1" case status == vcs.Added: hex = "#008800" case status == vcs.Deleted: hex = "#ff4252" case status == vcs.Conflicted: hex = "#ce8020" case status == vcs.Updated: hex = "#008060" case status == vcs.Stored: fn.Styles.Color = colors.Scheme.OnSurface } if fn.Info.Generated { hex = "#8080C0" } if hex != "" { fn.Styles.Color = colors.Uniform(colors.ToBase(errors.Must1(colors.FromHex(hex)))) } else { fn.Styles.Color = colors.Scheme.OnSurface } // if fn.Name == "test.go" { // rep, err := fn.Repo() // fmt.Println("style updt:", status, hex, rep != nil, err) // } } // 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 } // 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 isFS := false if fn.FileRoot().FS == nil { di = errors.Log1(os.ReadDir(path)) } else { isFS = true 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, isFS) // note: FS = ascending, otherwise descending sortByModTime(files, isFS) } files = append(dirs, files...) } else { if doModSort { sortByModTime(files, isFS) } } return files } // sortByModTime sorts by _reverse_ mod time (newest first) func sortByModTime(files []fs.FileInfo, ascending bool) { slices.SortFunc(files, func(a, b fs.FileInfo) int { if ascending { return a.ModTime().Compare(b.ModTime()) } return b.ModTime().Compare(a.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.DirRepo.StatusFast(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 } // 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.Delete() }) } // 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 } 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) 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]" ) // Treer is an interface for getting the Root node if it implements [Treer]. type Treer interface { AsFileTree() *Tree } // AsTree returns the given value as a [Tree] if it has // an AsFileTree() method, or nil otherwise. func AsTree(n tree.Node) *Tree { if t, ok := n.(Treer); ok { return t.AsFileTree() } return nil } // 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.Root = ft ft.FileNodeType = types.For[Node]() ft.OpenDepth = 4 ft.DirsOnTop = true ft.FirstMaker(func(p *tree.Plan) { if len(ft.externalFiles) == 0 { return } tree.AddNew(p, externalFilesName, func() Filer { return tree.NewOfType(ft.FileNodeType).(Filer) }, func(wf Filer) { w := wf.AsFileNode() w.Root = ft.Root 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() } func (ft *Tree) AsFileTree() *Tree { return ft } // 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() 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) } // OpenPaths returns a list of open paths. func (ft *Tree) OpenPaths() []string { return ft.Dirs.openPaths(string(ft.Filepath)) } // 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) } newExt := len(ft.externalFiles) == 0 ft.externalFiles = append(ft.externalFiles, pth) if newExt { ft.Update() } else { 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, errors.New("ExtFile not updated -- no ExtFiles node") } if n := ekid.AsTree().Child(i); n != nil { return AsNode(n), nil } return nil, errors.New("ExtFile not updated; index invalid") } // Code generated by "core generate"; DO NOT EDIT. package filetree import ( "io/fs" "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.\nThis allows apps to intervene and apply any additional logic for these actions.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "AsFileNode", Doc: "AsFileNode returns the [Node]", Returns: []string{"Node"}}, {Name: "RenameFiles", Doc: "RenameFiles renames any selected files."}, {Name: "DeleteFiles", Doc: "DeleteFiles deletes any selected files."}, {Name: "GetFileInfo", Doc: "GetFileInfo updates the .Info for this file", Returns: []string{"error"}}, {Name: "OpenFile", Doc: "OpenFile opens the file for node. This is called by OpenFilesDefault", Returns: []string{"error"}}}}) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/filetree.Node", IDName: "node", Doc: "Node represents a file in the file system, as a [core.Tree] node.\nThe name of the node is the name of the file.\nFolders have children containing further nodes.", Directives: []types.Directive{{Tool: "core", Directive: "embedder"}}, Methods: []types.Method{{Name: "Cut", Doc: "Cut copies the selected files to the clipboard and then deletes them.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Paste", Doc: "Paste inserts files from the clipboard.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "OpenFilesDefault", Doc: "OpenFilesDefault opens selected files with default app for that file type (os defined).\nruns open on Mac, xdg-open on Linux, and start on Windows", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "duplicateFiles", Doc: "duplicateFiles makes a copy of selected files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "DeleteFiles", Doc: "DeleteFiles deletes any selected files or directories. If any directory is selected,\nall files and subdirectories in that directory are also deleted.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "RenameFiles", Doc: "renames any selected files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "RenameFile", Doc: "RenameFile renames file to new name", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"newpath"}, Returns: []string{"error"}}, {Name: "newFiles", Doc: "newFiles makes a new file in selected directory", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename", "addToVCS"}}, {Name: "newFile", Doc: "newFile makes a new file in this directory node", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename", "addToVCS"}}, {Name: "newFolders", Doc: "makes a new folder in the given selected directory", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"foldername"}}, {Name: "newFolder", Doc: "newFolder makes a new folder (directory) in this directory node", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"foldername"}}, {Name: "showFileInfo", Doc: "Shows file information about selected file(s)", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "sortBys", Doc: "sortBys determines how to sort the selected files in the directory.\nDefault is alpha by name, optionally can be sorted by modification time.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"modTime"}}, {Name: "openAll", Doc: "openAll opens all directories under this one", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "removeFromExterns", Doc: "removeFromExterns removes file from list of external files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "addToVCSSelected", Doc: "addToVCSSelected adds selected files to version control system", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "deleteFromVCSSelected", Doc: "deleteFromVCSSelected removes selected files from version control system", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "commitToVCSSelected", Doc: "commitToVCSSelected commits to version control system based on last selected file", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "revertVCSSelected", Doc: "revertVCSSelected removes selected files from version control system", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "diffVCSSelected", Doc: "diffVCSSelected shows the diffs between two versions of selected files, given by the\nrevision specifiers -- if empty, defaults to A = current HEAD, B = current WC file.\n-1, -2 etc also work as universal ways of specifying prior revisions.\nDiffs are shown in a DiffEditorDialog.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"rev_a", "rev_b"}}, {Name: "logVCSSelected", Doc: "logVCSSelected shows the VCS log of commits for selected files.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "blameVCSSelected", Doc: "blameVCSSelected shows the VCS blame report for this file, reporting for each line\nthe revision and author of the last change.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Tree"}}, Fields: []types.Field{{Name: "Filepath", Doc: "Filepath is the full path to this file."}, {Name: "Info", Doc: "Info is the full standard file info about this file."}, {Name: "FileIsOpen", Doc: "FileIsOpen indicates that this file has been opened, indicated by Italics."}, {Name: "DirRepo", Doc: "DirRepo is the version control system repository for this directory,\nonly non-nil if this is the highest-level directory in the tree under vcs control."}, {Name: "repoFiles", Doc: "repoFiles has the version control system repository file status,\nproviding a much faster way to get file status, vs. the repo.Status\ncall which is exceptionally slow."}}}) // 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 } // SetFileIsOpen sets the [Node.FileIsOpen]: // FileIsOpen indicates that this file has been opened, indicated by Italics. func (t *Node) SetFileIsOpen(v bool) *Node { t.FileIsOpen = v; 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: "Dirs 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: "DirsOnTop indicates whether all directories are placed at the top of the tree.\nOtherwise everything is mixed. This is the default."}, {Name: "SortByModTime", Doc: "SortByModTime causes files to be sorted by modification time by default.\nOtherwise it is a per-directory option."}, {Name: "FileNodeType", Doc: "FileNodeType is the type of node to create; defaults to [Node] but can use custom node types"}, {Name: "FilterFunc", Doc: "FilterFunc, if set, determines whether to include the given node in the tree.\nreturn true to include, false to not. This applies to files and directories alike."}, {Name: "FS", Doc: "FS is the file system we are browsing, if it is an FS (nil = os filesystem)"}, {Name: "inOpenAll", Doc: "inOpenAll indicates whether we are in midst of an OpenAll call; nodes should open all dirs."}, {Name: "watcher", Doc: "watcher does change notify for all dirs"}, {Name: "doneWatcher", Doc: "doneWatcher is channel to close watcher watcher"}, {Name: "watchedPaths", Doc: "watchedPaths is map of paths that have been added to watcher; only active if bool = true"}, {Name: "lastWatchUpdate", Doc: "lastWatchUpdate is last path updated by watcher"}, {Name: "lastWatchTime", Doc: "lastWatchTime is 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]: // DirsOnTop indicates whether all directories are placed at the top of the tree. // Otherwise everything is mixed. This is the default. func (t *Tree) SetDirsOnTop(v bool) *Tree { t.DirsOnTop = v; return t } // SetSortByModTime sets the [Tree.SortByModTime]: // SortByModTime causes files to be sorted by modification time by default. // Otherwise it is a per-directory option. func (t *Tree) SetSortByModTime(v bool) *Tree { t.SortByModTime = v; return t } // SetFileNodeType sets the [Tree.FileNodeType]: // FileNodeType is the 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 } // SetFilterFunc sets the [Tree.FilterFunc]: // 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. func (t *Tree) SetFilterFunc(v func(path string, info fs.FileInfo) bool) *Tree { t.FilterFunc = v return t } // SetFS sets the [Tree.FS]: // FS is the file system we are browsing, if it is an FS (nil = os filesystem) func (t *Tree) SetFS(v fs.FS) *Tree { t.FS = 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/text/lines" "cogentcore.org/core/text/text" "cogentcore.org/core/text/textcore" "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() 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. returns true if a repository was newly found here. func (fn *Node) detectVCSRepo() 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 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) { fr := fn.FileRoot() if fr == nil { return nil, nil } if fn.isExternal() || fr == nil || fr.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.DirRepo.Files(func(f vcs.Files) { // need the func to make it work fr := fn.FileRoot() fr.AsyncLock() fn.Update() fr.AsyncUnlock() }) } // 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 } 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 } // todo: // if fn.Lines != nil { // fn.Lines.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)) } // todo: _, err := textcore.DiffEditorDialogFromRevs(fn, repo, string(fn.Filepath) /*fn.Lines*/, nil, 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) *textcore.TwinEditors { title := "VCS Blame: " + fsx.DirAndFile(fname) d := core.NewBody(title) tv := textcore.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 = text.WhiteSpacePre s.Min.X.Ch(30) s.Min.Y.Em(40) }) tvb.Styler(func(s *styles.Style) { s.Text.WhiteSpace = text.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 := lines.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() { 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/text/diffbrowser" "cogentcore.org/core/text/lines" "cogentcore.org/core/text/textcore" "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 := lines.NewLines() buf.SetFilename(lv.File) buf.Settings.LineNumbers = true buf.Stat() textcore.NewEditor(d).SetLines(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 { textcore.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 := lines.NewLines().SetText(fb).SetFilename(file) // file is key for getting lang textcore.NewEditor(d).SetLines(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 ( "io" "net/http" "strings" "cogentcore.org/core/base/errors" "cogentcore.org/core/colors" "cogentcore.org/core/core" "cogentcore.org/core/events" "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" "github.com/gomarkdown/markdown/ast" "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 // TableParent is the current table being generated. TableParent *core.Frame firstRow bool // 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) // 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. ElementHandlers map[string]func(ctx *Context) bool // WikilinkHandlers is a list of handlers to use for wikilinks. // If one returns "", "", the next ones will be tried instead. // The functions are tried in sequential ascending order. // See [Context.AddWikilinkHandler] to add a new handler. WikilinkHandlers []WikilinkHandler // AttributeHandlers is a map of markdown render handler functions // for custom attribute values that are specified in {tag: value} // attributes prior to markdown elements in the markdown source. // The map key is the tag in the attribute, which is then passed // to the function, along with the markdown node being rendered. // Alternative or additional HTML output can be written to the given writer. // If the handler function returns true, then the default HTML code // will not be generated. AttributeHandlers map[string]func(ctx *Context, w io.Writer, node ast.Node, entering bool, tag, value string) bool } // 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, ElementHandlers: map[string]func(ctx *Context) bool{}, AttributeHandlers: map[string]func(ctx *Context, w io.Writer, node ast.Node, entering bool, tag, value string) bool{}, } } // 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.FromProperty(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) } } } // LinkButton is a helper function that makes the given button // open the given link when clicked on, using [Context.OpenURL]. // The advantage of using this is that it does [tree.NodeBase.SetProperty] // of "href" to the given url, allowing generatehtml to create an <a> element // for HTML preview and SEO purposes. // // See also [Context.LinkButtonUpdating] for a dynamic version. func (c *Context) LinkButton(bt *core.Button, url string) *core.Button { bt.SetProperty("tag", "a") bt.SetProperty("href", url) bt.OnClick(func(e events.Event) { c.OpenURL(url) }) return bt } // LinkButtonUpdating is a version of [Context.LinkButton] that is robust to a changing/dynamic // URL, using an Updater and a URL function instead of a variable. func (c *Context) LinkButtonUpdating(bt *core.Button, url func() string) *core.Button { bt.SetProperty("tag", "a") bt.Updater(func() { bt.SetProperty("href", url()) }) bt.OnClick(func(e events.Event) { c.OpenURL(url()) }) 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 ( _ "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" _ "cogentcore.org/core/text/tex" // include this to get math ) //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/styles" "cogentcore.org/core/styles/states" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/lines" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" "cogentcore.org/core/text/textcore" "cogentcore.org/core/tree" "golang.org/x/net/html" ) // 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 [Context.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 := ctx.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 switch tag { case "body": w.Styler(func(s *styles.Style) { s.Grow.Set(1, 1) }) case "ol", "ul": w.Styler(func(s *styles.Style) { s.Grow.Set(1, 0) }) case "div": w.Styler(func(s *styles.Style) { s.Grow.Set(1, 1) s.Overflow.Y = styles.OverflowAuto }) case "blockquote": w.Styler(func(s *styles.Style) { // todo: need a better marker s.Grow.Set(1, 0) s.Background = colors.Scheme.SurfaceContainer }) } 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 { codeEl := ctx.Node.FirstChild collapsed := GetAttr(codeEl, "collapsed") lang := getLanguage(GetAttr(codeEl, "class")) id := GetAttr(codeEl, "id") var ed *textcore.Editor var parent tree.Node if collapsed != "" { cl := New[core.Collapser](ctx) summary := core.NewText(cl.Summary).SetText("Code") if title := GetAttr(codeEl, "title"); title != "" { summary.SetText(title) } ed = textcore.NewEditor(cl.Details) if id != "" { cl.Summary.Name = id } parent = cl.Parent if collapsed == "false" || collapsed == "-" { cl.Open = true } } else { ed = New[textcore.Editor](ctx) if id != "" { ed.SetName(id) } parent = ed.Parent } ctx.Node = codeEl if lang != "" { ed.Lines.SetFileExt(lang) } ed.Lines.SetString(ExtractText(ctx)) if BindTextEditor != nil && (lang == "Go" || lang == "Goal") { ed.Lines.SpacesToTabs(0, ed.Lines.NumLines()) // Go uses tabs parFrame := core.NewFrame(parent) parFrame.Styler(func(s *styles.Style) { s.Direction = styles.Column s.Grow.Set(1, 0) }) // we inherit our Grow.Y from our first child so that // elements that want to grow can do so parFrame.SetOnChildAdded(func(n tree.Node) { if _, ok := n.(*core.Body); ok { // Body should not grow return } wb := core.AsWidget(n) if wb.IndexInParent() != 0 { return } wb.FinalStyler(func(s *styles.Style) { parFrame.Styles.Grow.Y = s.Grow.Y }) }) BindTextEditor(ed, parFrame, lang) } else { ed.SetReadOnly(true) ed.Lines.Settings.LineNumbers = false ed.Styler(func(s *styles.Style) { s.Border.Width.Zero() s.MaxBorder.Width.Zero() s.StateLayer = 0 s.Background = colors.Scheme.SurfaceContainer }) } } else { handleText(ctx).Styler(func(s *styles.Style) { s.Text.WhiteSpace = text.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. hasPChild := false if ctx.Node.FirstChild != nil && ctx.Node.FirstChild.Data == "p" { ctx.Node = ctx.Node.FirstChild hasPChild = true } 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) if hasPChild { // handle potential additional <p> blocks that should be indented cnode := ctx.Node ctx.BlockParent = text.Parent.(core.Widget) for cnode.NextSibling != nil { cnode = cnode.NextSibling ctx.Node = cnode if cnode.Data != "p" { continue } txt := handleText(ctx) txt.SetText(" " + txt.Text) } } case "img": n := ctx.Node src := GetAttr(n, "src") alt := GetAttr(n, "alt") pid := "" if ctx.BlockParent != nil { pid = GetAttr(n.Parent, "id") } // Can be either image or svg. var img *core.Image var svg *core.SVG if strings.HasSuffix(src, ".svg") { svg = New[core.SVG](ctx) svg.SetTooltip(alt) if pid != "" { svg.SetName(pid) } } else { img = New[core.Image](ctx) img.SetTooltip(alt) if pid != "" { img.SetName(pid) } } go func() { resp, err := Get(ctx, src) if errors.Log(err) != nil { return } defer resp.Body.Close() if svg != nil { svg.AsyncLock() svg.Read(resp.Body) svg.Update() svg.AsyncUnlock() } else { im, _, err := imagex.Read(resp.Body) if err != nil { slog.Error("error loading image", "url", src, "err", err) return } img.AsyncLock() img.SetImage(im) img.Update() img.AsyncUnlock() } }() 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 := lines.NewLines() buf.SetText([]byte(ExtractText(ctx))) New[textcore.Editor](ctx).SetLines(buf) case "table": w := New[core.Frame](ctx) ctx.NewParent = w ctx.TableParent = w ctx.firstRow = true w.SetProperty("cols", 0) w.Styler(func(s *styles.Style) { s.Display = styles.Grid s.Grow.Set(1, 1) s.Columns = w.Property("cols").(int) s.Gap.X.Dp(core.ConstantSpacing(6)) s.Justify.Content = styles.Center }) case "th", "td": if ctx.TableParent != nil && ctx.firstRow { cols := ctx.TableParent.Property("cols").(int) cols++ ctx.TableParent.SetProperty("cols", cols) } tx := handleText(ctx) if tag == "th" { tx.Styler(func(s *styles.Style) { s.Font.Weight = rich.Bold s.Border.Width.Bottom.Dp(2) s.Margin.Bottom.Dp(6) s.Margin.Top.Dp(6) }) } else { tx.Styler(func(s *styles.Style) { s.Margin.Bottom.Dp(6) s.Margin.Top.Dp(6) }) } case "thead", "tbody": ctx.NewParent = ctx.TableParent case "tr": if ctx.TableParent != nil && ctx.firstRow && ctx.TableParent.NumChildren() > 0 { ctx.firstRow = false } ctx.NewParent = ctx.TableParent default: ctx.NewParent = ctx.Parent() } } func (ctx *Context) textStyler(s *styles.Style) { s.Margin.SetVertical(units.Em(core.ConstantSpacing(0.25))) s.Font.Size.Value *= core.AppearanceSettings.DocsFontSize / 100 // TODO: it would be ideal for htmlcore to automatically save a scale factor // in general and for each domain, that is applied only to page content // scale := float32(1.2) // s.Font.Size.Value *= scale // s.Text.LineHeight.Value *= scale // s.Text.LetterSpacing.Value *= scale } // 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 { et := ExtractText(ctx) if et == "" { // Empty text elements do not render, so we just return a fake one (to avoid panics). return core.NewText() } tx := New[core.Text](ctx).SetText(et) tx.Styler(ctx.textStyler) tx.HandleTextClick(func(tl *rich.Hyperlink) { 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(ctx.textStyler) tx.HandleTextClick(func(tl *rich.Hyperlink) { 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 } // BindTextEditor is a function set to [cogentcore.org/core/yaegicore.BindTextEditor] // when importing yaegicore, which provides interactive editing functionality for Go // code blocks in text editors. var BindTextEditor func(ed *textcore.Editor, parent *core.Frame, language string) // 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" "io" "regexp" "cogentcore.org/core/core" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/ast" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" ) var divRegex = regexp.MustCompile("<p(.*?)><div></p>") func mdToHTML(ctx *Context, md []byte) []byte { // create markdown parser with extensions extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.Attributes | parser.Mmark p := parser.NewWithExtensions(extensions) prev := p.RegisterInline('[', nil) p.RegisterInline('[', wikilink(ctx, prev)) // this allows div to work properly: // https://github.com/gomarkdown/markdown/issues/5 md = bytes.ReplaceAll(md, []byte("</div>"), []byte("</div><!-- dummy -->")) doc := p.Parse(md) // create HTML renderer with extensions htmlFlags := html.CommonFlags | html.HrefTargetBlank opts := html.RendererOptions{Flags: htmlFlags, RenderNodeHook: ctx.mdRenderHook} renderer := html.NewRenderer(opts) htm := markdown.Render(doc, renderer) htm = bytes.ReplaceAll(htm, []byte("<p></div><!-- dummy --></p>"), []byte("</div>")) htm = divRegex.ReplaceAll(htm, []byte("<div${1}>")) return htm } // ReadMD reads MD (markdown) from the given bytes and adds corresponding // Cogent Core widgets to the given [core.Widget], using the given context. func ReadMD(ctx *Context, parent core.Widget, b []byte) error { htm := mdToHTML(ctx, b) // os.WriteFile("htmlcore_tmp.html", htm, 0666) // note: keep here, needed for debugging 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)) } func (ctx *Context) attrRenderHooks(attr *ast.Attribute, w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { for tag, val := range attr.Attrs { f, has := ctx.AttributeHandlers[tag] if has { b := f(ctx, w, node, entering, tag, string(val)) return ast.GoToNext, b } } return ast.GoToNext, false } func (ctx *Context) mdRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { cont := node.AsContainer() if cont != nil && cont.Attribute != nil { return ctx.attrRenderHooks(cont.Attribute, w, node, entering) } leaf := node.AsLeaf() if leaf != nil && leaf.Attribute != nil { return ctx.attrRenderHooks(leaf.Attribute, w, node, entering) } return ast.GoToNext, false } // MDGetAttr gets the given attribute from the given markdown node, returning "" // if the attribute is not found. func MDGetAttr(n ast.Node, attr string) string { res := "" cont := n.AsContainer() leaf := n.AsLeaf() if cont != nil { if cont.Attribute != nil { res = string(cont.Attribute.Attrs[attr]) } } else if leaf != nil { if leaf.Attribute != nil { res = string(leaf.Attribute.Attrs[attr]) } } return res } // MDSetAttr sets the given attribute on the given markdown node func MDSetAttr(n ast.Node, attr, value string) { var attrs *ast.Attribute cont := n.AsContainer() leaf := n.AsLeaf() if cont != nil { attrs = cont.Attribute } else if leaf != nil { attrs = leaf.Attribute } if attrs == nil { attrs = &ast.Attribute{} } if attrs.Attrs == nil { attrs.Attrs = make(map[string][]byte) } attrs.Attrs[attr] = []byte(value) if cont != nil { cont.Attribute = attrs } else if leaf != nil { leaf.Attribute = attrs } } // 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) // readHTMLNode already handles children and siblings, so we return. // TODO: for something like [TestButtonInHeadingBug] this will not // have the right behavior, but that is a rare use case and this // heuristic is much simpler. return str } if 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 ( "io/fs" "net/http" "net/url" "strings" ) // 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 } // GetURLFromFS can be used for [Context.GetURL] to get // resources from the given file system. func GetURLFromFS(fsys fs.FS, 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, "/") rawURL, _, _ = strings.Cut(rawURL, "?") f, err := fsys.Open(rawURL) if err != nil { return nil, err } return &http.Response{ Status: http.StatusText(http.StatusOK), StatusCode: http.StatusOK, Body: f, ContentLength: -1, }, 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 ( "log/slog" "strings" "unicode" "github.com/gomarkdown/markdown/ast" "github.com/gomarkdown/markdown/parser" ) // WikilinkHandler is a function that converts wikilink text to // a corresponding URL and label text. If it returns "", "", the // handler will be skipped in favor of the next possible handlers. // Wikilinks are of the form [[wikilink text]]. Only the text inside // of the brackets is passed to the handler. If there is additional // text directly after the closing brackets without spaces or punctuation, // it will be appended to the label text after the handler is run // (ex: [[widget]]s). type WikilinkHandler func(text string) (url string, label string) // AddWikilinkHandler adds a new [WikilinkHandler] to [Context.WikilinkHandlers]. // If it returns "", "", the next handlers will be tried instead. // The functions are tried in sequential ascending order. func (c *Context) AddWikilinkHandler(h WikilinkHandler) { c.WikilinkHandlers = append(c.WikilinkHandlers, h) } // GoDocWikilink returns a [WikilinkHandler] that converts wikilinks of the form // [[prefix:identifier]] to a pkg.go.dev URL starting at pkg. For example, with // prefix="doc" and pkg="cogentcore.org/core", the wikilink [[doc:core.Button]] will // result in the URL "https://pkg.go.dev/cogentcore.org/core/core#Button". func GoDocWikilink(prefix string, pkg string) WikilinkHandler { return func(text string) (url string, label string) { if !strings.HasPrefix(text, prefix+":") { return "", "" } text = strings.TrimPrefix(text, prefix+":") // pkg.go.dev uses fragments for first dot within package t := strings.Replace(text, ".", "#", 1) url = "https://pkg.go.dev/" + pkg + "/" + t return url, text } } // 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(ctx *Context, 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:] // minimum: [[X]] if len(data) < 5 || data[1] != '[' { return fn(p, original, offset) } inside, after := getWikilinkText(data) url, label := runWikilinkHandlers(ctx, inside) var node ast.Node if len(url) == 0 && len(label) == 0 { slog.Error("invalid wikilink", "link", string(inside)) // TODO: we just treat broken wikilinks like plaintext for now, but we should // make red links instead at some point node = &ast.Text{Leaf: ast.Leaf{Literal: append(inside, after...)}} } else { node = &ast.Link{Destination: url} ast.AppendChild(node, &ast.Text{Leaf: ast.Leaf{Literal: append(label, after...)}}) } return len(inside) + len(after) + 4, node } } // getWikilinkText gets the wikilink text from the given raw text data starting with [[. // Inside contains the text inside the [[]], and after contains all of the text // after the ]] until there is a space or punctuation. func getWikilinkText(data []byte) (inside, after []byte) { i := 2 for ; i < len(data); i++ { if data[i] == ']' && data[i-1] == ']' { inside = data[2 : i-1] continue } r := rune(data[i]) // Space or punctuation after ]] means we are done. if inside != nil && (unicode.IsSpace(r) || unicode.IsPunct(r)) { break } } after = data[len(inside)+4 : i] return } // runWikilinkHandlers returns the first non-blank URL and label returned // by [Context.WikilinkHandlers]. func runWikilinkHandlers(ctx *Context, text []byte) (url, label []byte) { for _, h := range ctx.WikilinkHandlers { u, l := h(string(text)) if u == "" && l == "" { continue } url, label = []byte(u), []byte(l) break } 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 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]struct{}{} // 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 } // ToFixed returns fixed.Rectangle26_6 version of this bbox. func (b Box2) ToFixed() fixed.Rectangle26_6 { rect := fixed.Rectangle26_6{Min: b.Min.ToFixed(), Max: b.Max.ToFixed()} 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 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 // 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 ( "cmp" "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[T cmp.Ordered](x, a, b T) T { 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. // [XX YX] // [XY YY] // [X0 Y0] 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. // This uses the standard graphics convention where increasing Y goes _down_ instead // of up, in contrast with the mathematical coordinate system where Y is up. func Rotate2D(angle float32) Matrix2 { s, c := Sincos(angle) return Matrix2{ c, s, -s, c, 0, 0, } } // Rotate2DAround returns a Matrix2 2D matrix with given rotation, specified in radians, // around given offset point that is translated to and from. // This uses the standard graphics convention where increasing Y goes _down_ instead // of up, in contrast with the mathematical coordinate system where Y is up. func Rotate2DAround(angle float32, pos Vector2) Matrix2 { return Identity2().Translate(pos.X, pos.Y).Rotate(angle).Translate(-pos.X, -pos.Y) } // 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)) } // ScaleAbout adds a scaling transformation about (x,y) in sx and sy. // When scale is negative it will flip those axes. func (m Matrix2) ScaleAbout(sx, sy, x, y float32) Matrix2 { return m.Translate(x, y).Scale(sx, sy).Translate(-x, -y) } func (a Matrix2) Rotate(angle float32) Matrix2 { return a.Mul(Rotate2D(angle)) } // RotateAbout adds a rotation transformation about (x,y) // with rot in radians counter clockwise. func (m Matrix2) RotateAbout(rot, x, y float32) Matrix2 { return m.Translate(x, y).Rotate(rot).Translate(-x, -y) } 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 does a simple extraction of the rotation matrix for // a single rotation. See [Matrix2.Decompose] for two rotations. 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 _, _, _, scx, scy, _ = a.Decompose() return } // Pos returns the translation values, X0, Y0 func (a Matrix2) Pos() (tx, ty float32) { return a.X0, a.Y0 } // Decompose extracts the translation, rotation, scaling and rotation components // (applied in the reverse order) as (tx, ty, theta, sx, sy, phi) with rotation // counter clockwise. This corresponds to: // Identity.Translate(tx, ty).Rotate(phi).Scale(sx, sy).Rotate(theta). func (m Matrix2) Decompose() (tx, ty, phi, sx, sy, theta float32) { // see https://math.stackexchange.com/questions/861674/decompose-a-2d-arbitrary-transform-into-only-scaling-and-rotation E := (m.XX + m.YY) / 2.0 F := (m.XX - m.YY) / 2.0 G := (m.YX + m.XY) / 2.0 H := (m.YX - m.XY) / 2.0 Q, R := Sqrt(E*E+H*H), Sqrt(F*F+G*G) sx, sy = Q+R, Q-R a1, a2 := Atan2(G, F), Atan2(H, E) // note: our rotation matrix is inverted so we reverse the sign on these. theta = -(a2 - a1) / 2.0 phi = -(a2 + a1) / 2.0 if sx == 1 && sy == 1 { theta += phi phi = 0 } tx = m.X0 ty = m.Y0 return } // Transpose returns the transpose of the matrix func (a Matrix2) Transpose() Matrix2 { a.XY, a.YX = a.YX, a.XY return a } // Det returns the determinant of the matrix func (a Matrix2) Det() float32 { return a.XX*a.YY - a.XY*a.YX // ad - bc } // 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.Det() 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 } // mapping onto canvas, [col][row] matrix: // m[0][0] = XX // m[1][0] = YX // m[0][1] = XY // m[1][1] = YY // m[0][2] = X0 // m[1][2] = Y0 // Eigen returns the matrix eigenvalues and eigenvectors. // The first eigenvalue is related to the first eigenvector, // and so for the second pair. Eigenvectors are normalized. func (m Matrix2) Eigen() (float32, float32, Vector2, Vector2) { if Abs(m.YX) < 1.0e-7 && Abs(m.XY) < 1.0e-7 { return m.XX, m.YY, Vector2{1.0, 0.0}, Vector2{0.0, 1.0} } lambda1, lambda2 := solveQuadraticFormula(1.0, -m.XX-m.YY, m.Det()) if IsNaN(lambda1) && IsNaN(lambda2) { // either m.XX or m.YY is NaN or the the affine matrix has no real eigenvalues return lambda1, lambda2, Vector2{}, Vector2{} } else if IsNaN(lambda2) { lambda2 = lambda1 } // see http://www.math.harvard.edu/archive/21b_fall_04/exhibits/2dmatrices/index.html var v1, v2 Vector2 if m.YX != 0 { v1 = Vector2{lambda1 - m.YY, m.YX}.Normal() v2 = Vector2{lambda2 - m.YY, m.YX}.Normal() } else if m.XY != 0 { v1 = Vector2{m.XY, lambda1 - m.XX}.Normal() v2 = Vector2{m.XY, lambda2 - m.XX}.Normal() } return lambda1, lambda2, v1, v2 } // Numerically stable quadratic formula, lowest root is returned first, // see https://math.stackexchange.com/a/2007723 func solveQuadraticFormula(a, b, c float32) (float32, float32) { if a == 0 { if b == 0 { if c == 0 { // all terms disappear, all x satisfy the solution return 0.0, NaN() } // linear term disappears, no solutions return NaN(), NaN() } // quadratic term disappears, solve linear equation return -c / b, NaN() } if c == 0 { // no constant term, one solution at zero and one from solving linearly if b == 0 { return 0.0, NaN() } return 0.0, -b / a } discriminant := b*b - 4.0*a*c if discriminant < 0.0 { return NaN(), NaN() } else if discriminant == 0 { return -b / (2.0 * a), NaN() } // Avoid catastrophic cancellation, which occurs when we subtract // two nearly equal numbers and causes a large error. // This can be the case when 4*a*c is small so that sqrt(discriminant) -> b, // and the sign of b and in front of the radical are the same. // Instead, we calculate x where b and the radical have different signs, // and then use this result in the analytical equivalent of the formula, // called the Citardauq Formula. q := Sqrt(discriminant) if b < 0.0 { // apply sign of b q = -q } x1 := -(b + q) / (2.0 * a) x2 := c / (a * x1) if x2 < x1 { x1, x2 = x2, x1 } return x1, x2 } // 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" "math" ) //gosl:start 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 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 = math.Inf(-1) 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" "cogentcore.org/core/math32" ) //gosl:start // 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 +Inf, Max to -Inf -- suitable for // iteratively calling Fit*InRange. See also Sanitize when done. func (mr *F32) SetInfinity() { mr.Min = math32.Inf(1) mr.Max = math32.Inf(-1) } // 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.ClampValue(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()) } // ClampValue clamps given value within Min / Max range // Note: a NaN will remain as a NaN. func (mr *F32) ClampValue(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 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 } // Sanitize ensures that the Min / Max range is not infinite or contradictory. func (mr *F32) Sanitize() { if math32.IsInf(mr.Min, 0) { mr.Min = 0 } if math32.IsInf(mr.Max, 0) { mr.Max = 0 } if mr.Min > mr.Max { mr.Min, mr.Max = mr.Max, mr.Min } if mr.Min == mr.Max { mr.Min-- mr.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 minmax provides a struct that holds Min and Max values. package minmax import "math" //go:generate core generate // 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 +Inf, Max to -Inf, suitable for // iteratively calling Fit*InRange. See also Sanitize when done. func (mr *F64) SetInfinity() { mr.Min = math.Inf(1) mr.Max = math.Inf(-1) } // 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.ClampValue(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()) } // ClampValue clips given value within Min / Max range // Note: a NaN will remain as a NaN func (mr *F64) ClampValue(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 } // Sanitize ensures that the Min / Max range is not infinite or contradictory. func (mr *F64) Sanitize() { if math.IsInf(mr.Min, 0) { mr.Min = 0 } if math.IsInf(mr.Max, 0) { mr.Max = 0 } if mr.Min > mr.Max { mr.Min, mr.Max = mr.Max, mr.Min } if mr.Min == mr.Max { mr.Min-- mr.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 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.Clamp(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) Clamp(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 { 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) *Range32 { rr.FixMin = true rr.Min = mn return rr } // SetMax sets a fixed max value func (rr *Range32) SetMax(mx float32) *Range32 { rr.FixMax = true rr.Max = mx return rr } // Range returns Max - Min func (rr *Range32) Range() float32 { return rr.Max - rr.Min } // Clamp returns min, max values clamped according to Fixed min / max of range. func (rr *Range32) Clamp(mnIn, mxIn float32) (mn, mx float32) { mn, mx = mnIn, mxIn if rr.FixMin && rr.Min < mn { mn = rr.Min } if rr.FixMax && rr.Max > mx { mx = rr.Max } return } /////////////////////////////////////////////////////////////////////// // 64 // Range64 represents a range of values for plotting, where the min or max can optionally be fixed type Range64 struct { 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) *Range64 { rr.FixMin = true rr.Min = mn return rr } // SetMax sets a fixed max value func (rr *Range64) SetMax(mx float64) *Range64 { rr.FixMax = true rr.Max = mx return rr } // Range returns Max - Min func (rr *Range64) Range() float64 { return rr.Max - rr.Min } // Clamp returns min, max values clamped according to Fixed min / max of range. func (rr *Range64) Clamp(mnIn, mxIn float64) (mn, mx float64) { mn, mx = mnIn, mxIn if rr.FixMin && rr.Min < mn { mn = rr.Min } if rr.FixMax && rr.Max > mx { mx = rr.Max } return } // 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 IsNaN(t) { 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 } if tymin > tmin || IsNaN(tmin) { tmin = tymin } if tymax < tmax || IsNaN(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 || IsNaN(tmin) { tmin = tzmin } if tzmax < tmax || IsNaN(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} } 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" "github.com/chewxy/math32" "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} } // Vector2Polar returns a new [Vector2] from polar coordinates, // with angle in radians CCW and radius the distance from (0,0). func Vector2Polar(angle, radius float32) Vector2 { return Vector2{radius * math32.Cos(angle), radius * math32.Sin(angle)} } // 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) } 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 { l := v.Length() if l == 0 { return Vector2{} } return v.DivScalar(l) } // 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 } // Rot90CW rotates the line OP by 90 degrees CW. func (v Vector2) Rot90CW() Vector2 { return Vector2{v.Y, -v.X} } // Rot90CCW rotates the line OP by 90 degrees CCW. func (v Vector2) Rot90CCW() Vector2 { return Vector2{-v.Y, v.X} } // Rot rotates the line OP by phi radians CCW. func (v Vector2) Rot(phi float32, p0 Vector2) Vector2 { sinphi, cosphi := math32.Sincos(phi) return Vector2{ p0.X + cosphi*(v.X-p0.X) - sinphi*(v.Y-p0.Y), p0.Y + sinphi*(v.X-p0.X) + cosphi*(v.Y-p0.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 // 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} } 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) } 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} } 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) } 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) 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 ( "cogentcore.org/core/math32" ) // 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" "cogentcore.org/core/styles/sides" ) // StandardBox 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 *Painter) StandardBox(st *styles.Style, pos math32.Vector2, size math32.Vector2, pabg image.Image) { if !st.RenderBox || size == (math32.Vector2{}) { return } encroach, pr := pc.boundsEncroachParent(pos, size) tm := st.TotalMargin().Round() mpos := pos.Add(tm.Pos()) msize := size.Sub(tm.Size()) if msize == (math32.Vector2{}) { return } 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.Fill.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.Fill.Color = pabg pc.RoundedRectangleSides(pos.X, pos.Y, size.X, size.Y, radius) pc.Draw() } else { pc.BlitBox(pos, size, pabg) } } pc.Stroke.Opacity = st.Opacity // pc.Font.Opacity = st.Opacity // todo: // 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.Stroke.Color = nil // note: applying 0.5 here does a reasonable job of matching // material design shadows, at their specified alpha levels. pc.Fill.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.RoundedShadowBlur(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 sides.AreZero(radius.Sides) { pc.FillBox(mpos, msize, st.ActualBackground) } else { pc.Fill.Color = st.ActualBackground // no border; fill on pc.RoundedRectangleSides(mpos.X, mpos.Y, msize.X, msize.Y, radius) pc.Draw() } // 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.Fill.Color = nil pc.Border(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 *Painter) boundsEncroachParent(pos, size math32.Vector2) (bool, sides.Floats) { if len(pc.Stack) <= 1 { return false, sides.Floats{} } ctx := pc.Stack[len(pc.Stack)-2] pr := ctx.Bounds.Radius if sides.AreZero(pr.Sides) { return false, pr } pbox := ctx.Bounds.Rect.ToRect() psz := ctx.Bounds.Rect.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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package paint import ( "image" "image/color" "cogentcore.org/core/colors" "cogentcore.org/core/colors/gradient" "cogentcore.org/core/math32" "cogentcore.org/core/paint/pimage" "cogentcore.org/core/paint/render" "cogentcore.org/core/styles" "cogentcore.org/core/styles/sides" "cogentcore.org/core/text/shaped" "golang.org/x/image/draw" ) /* The original version borrowed heavily from: https://github.com/fogleman/gg Copyright (C) 2016 Michael Fogleman and https://github.com/srwiley/rasterx: Copyright 2018 by the rasterx Authors. All rights reserved. Created 2018 by S.R.Wiley The new version is more strongly based on https://github.com/tdewolff/canvas Copyright (c) 2015 Taco de Wolff, under an MIT License. */ // Painter provides the rendering state, styling parameters, and methods for // painting. It accumulates all painting actions in a [render.Render] // list, which should be obtained by a call to the [Painter.RenderDone] method // when done painting (resets list to start fresh). // // Pass this [render.Render] list to one or more [render.Renderers] to actually // generate the resulting output. Renderers are independent of the Painter // and the [render.Render] state is entirely self-contained, so rendering // can be done in a separate goroutine etc. // // You must import _ "cogentcore.org/core/paint/renderers" to get the default // renderers if using this outside of core which already does this for you. // This sets the New*Renderer functions to point to default implementations. type Painter struct { *State *styles.Paint } // NewPainter returns a new [Painter] with default styles and given size. func NewPainter(size math32.Vector2) *Painter { pc := &Painter{&State{}, styles.NewPaint()} pc.State.Init(pc.Paint, size) pc.SetUnitContextExt(size.ToPointCeil()) return pc } func (pc *Painter) Transform() math32.Matrix2 { return pc.Context().Transform.Mul(pc.Paint.Transform) } //////// Path basics // MoveTo starts a new subpath within the current path starting at the // specified point. func (pc *Painter) MoveTo(x, y float32) { pc.State.Path.MoveTo(x, y) } // 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 *Painter) LineTo(x, y float32) { pc.State.Path.LineTo(x, y) } // QuadTo adds a quadratic Bézier path with control point (cpx,cpy) and end point (x,y). func (pc *Painter) QuadTo(cpx, cpy, x, y float32) { pc.State.Path.QuadTo(cpx, cpy, x, y) } // CubeTo adds a cubic Bézier path with control points // (cpx1,cpy1) and (cpx2,cpy2) and end point (x,y). func (pc *Painter) CubeTo(cp1x, cp1y, cp2x, cp2y, x, y float32) { pc.State.Path.CubeTo(cp1x, cp1y, cp2x, cp2y, x, y) } // ArcTo adds an arc with radii rx and ry, with rot the counter clockwise // rotation with respect to the coordinate system in radians, large and sweep booleans // (see https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Arcs), // and (x,y) the end position of the pen. The start position of the pen was // given by a previous command's end point. func (pc *Painter) ArcTo(rx, ry, rot float32, large, sweep bool, x, y float32) { pc.State.Path.ArcTo(rx, ry, rot, large, sweep, x, y) } // Close closes a (sub)path with a LineTo to the start of the path // (the most recent MoveTo command). It also signals the path closes // as opposed to being just a LineTo command, which can be significant // for stroking purposes for example. func (pc *Painter) Close() { pc.State.Path.Close() } // Draw puts the current path on the render stack, capturing the style // settings present at this point, which will be used to render the path, // and creates a new current path. func (pc *Painter) Draw() { pc.Paint.ToDots() pt := render.NewPath(pc.State.Path.Clone(), pc.Paint, pc.Context()) pc.Render.Add(pt) pc.State.Path.Reset() } //////// basic shape functions // note: the path shapes versions can be used when you want to add to an existing path // using ppath.Join. These functions produce distinct standalone shapes, starting with // a MoveTo generally. // Line adds a separate line (MoveTo, LineTo). func (pc *Painter) Line(x1, y1, x2, y2 float32) { pc.State.Path.Line(x1, y1, x2, y2) } // Polyline adds multiple connected lines, with no final Close. func (pc *Painter) Polyline(points ...math32.Vector2) { pc.State.Path.Polyline(points...) } // Polyline adds multiple connected lines, with no final Close, // with coordinates in Px units. func (pc *Painter) PolylinePx(points ...math32.Vector2) { pu := &pc.UnitContext sz := len(points) if sz < 2 { return } p := &pc.State.Path p.MoveTo(pu.PxToDots(points[0].X), pu.PxToDots(points[0].Y)) for i := 1; i < sz; i++ { p.LineTo(pu.PxToDots(points[i].X), pu.PxToDots(points[i].Y)) } } // Polygon adds multiple connected lines with a final Close. func (pc *Painter) Polygon(points ...math32.Vector2) { pc.Polyline(points...) pc.Close() } // Polygon adds multiple connected lines with a final Close, // with coordinates in Px units. func (pc *Painter) PolygonPx(points ...math32.Vector2) { pc.PolylinePx(points...) pc.Close() } // Rectangle adds a rectangle of width w and height h at position x,y. func (pc *Painter) Rectangle(x, y, w, h float32) { pc.State.Path.Rectangle(x, y, w, h) } // RoundedRectangle adds a rectangle of width w and height h // with rounded corners of radius r at postion x,y. // A negative radius will cast the corners inwards (i.e. concave). func (pc *Painter) RoundedRectangle(x, y, w, h, r float32) { pc.State.Path.RoundedRectangle(x, y, w, h, r) } // RoundedRectangleSides adds 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 *Painter) RoundedRectangleSides(x, y, w, h float32, r sides.Floats) { pc.State.Path.RoundedRectangleSides(x, y, w, h, r) } // BeveledRectangle adds a rectangle of width w and height h // with beveled corners at distance r from the corner. func (pc *Painter) BeveledRectangle(x, y, w, h, r float32) { pc.State.Path.BeveledRectangle(x, y, w, h, r) } // Circle adds a circle at given center coordinates of radius r. func (pc *Painter) Circle(cx, cy, r float32) { pc.Ellipse(cx, cy, r, r) } // Ellipse adds an ellipse at given center coordinates of radii rx and ry. func (pc *Painter) Ellipse(cx, cy, rx, ry float32) { pc.State.Path.Ellipse(cx, cy, rx, ry) } // CircularArc adds a circular arc centered at given coordinates with radius r // and theta0 and theta1 as the angles in degrees of the ellipse // (before rot is applied) between which the arc will run. // If theta0 < theta1, the arc will run in a CCW direction. // If the difference between theta0 and theta1 is bigger than 360 degrees, // one full circle will be drawn and the remaining part of diff % 360, // e.g. a difference of 810 degrees will draw one full circle and an arc // over 90 degrees. func (pc *Painter) CircularArc(x, y, r, theta0, theta1 float32) { pc.State.Path.EllipticalArc(x, y, r, r, 0, theta0, theta1) } // EllipticalArc adds an elliptical arc centered at given coordinates with // radii rx and ry, with rot the counter clockwise rotation in degrees, // and theta0 and theta1 the angles in degrees of the ellipse // (before rot is applied) between which the arc will run. // If theta0 < theta1, the arc will run in a CCW direction. // If the difference between theta0 and theta1 is bigger than 360 degrees, // one full circle will be drawn and the remaining part of diff % 360, // e.g. a difference of 810 degrees will draw one full circle and an arc // over 90 degrees. func (pc *Painter) EllipticalArc(x, y, rx, ry, rot, theta0, theta1 float32) { pc.State.Path.EllipticalArc(x, y, rx, ry, rot, theta0, theta1) } // Triangle adds a triangle of radius r pointing upwards. func (pc *Painter) Triangle(x, y, r float32) { pc.State.Path.RegularPolygon(3, r, true).Translate(x, y) // todo: just make these take a position. } // RegularPolygon adds a regular polygon with radius r. // It uses n vertices/edges, so when n approaches infinity // this will return a path that approximates a circle. // n must be 3 or more. The up boolean defines whether // the first point will point upwards or downwards. func (pc *Painter) RegularPolygon(x, y float32, n int, r float32, up bool) { pc.State.Path.RegularPolygon(n, r, up).Translate(x, y) } // RegularStarPolygon adds a regular star polygon with radius r. // It uses n vertices of density d. This will result in a // self-intersection star in counter clockwise direction. // If n/2 < d the star will be clockwise and if n and d are not coprime // a regular polygon will be obtained, possible with multiple windings. // n must be 3 or more and d 2 or more. The up boolean defines whether // the first point will point upwards or downwards. func (pc *Painter) RegularStarPolygon(x, y float32, n, d int, r float32, up bool) { pc.State.Path.RegularStarPolygon(n, d, r, up).Translate(x, y) } // StarPolygon returns a star polygon of n points with alternating // radius R and r. The up boolean defines whether the first point // will be point upwards or downwards. func (pc *Painter) StarPolygon(x, y float32, n int, R, r float32, up bool) { pc.State.Path.StarPolygon(n, R, r, up).Translate(x, y) } // Grid returns a stroked grid of width w and height h, // with grid line thickness r, and the number of cells horizontally // and vertically as nx and ny respectively. func (pc *Painter) Grid(x, y, w, h float32, nx, ny int, r float32) { pc.State.Path.Grid(w, y, nx, ny, r).Translate(x, y) } // ClampBorderRadius returns the given border radius clamped to fit based // on the given width and height of the object. func ClampBorderRadius(r sides.Floats, w, h float32) sides.Floats { 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 } // Border 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 *Painter) Border(x, y, w, h float32, bs styles.Border) { origStroke := pc.Stroke origFill := pc.Fill defer func() { pc.Stroke = origStroke pc.Fill = origFill }() r := bs.Radius.Dots() if sides.AreSame(bs.Style) && sides.AreSame(bs.Color) && sides.AreSame(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.Stroke.Color { pc.Stroke.Color = bs.Color.Top } pc.Stroke.Width = bs.Width.Top pc.Stroke.ApplyBorderStyle(bs.Style.Top) if sides.AreZero(r.Sides) { pc.Rectangle(x, y, w, h) } else { pc.RoundedRectangleSides(x, y, w, h, r) } pc.Draw() return } // use consistent rounded rectangle for fill, and then draw borders side by side pc.RoundedRectangleSides(x, y, w, h, r) pc.Draw() 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.MoveTo(xtli, ytl) // set the color if it is not the same as the already set color if bs.Color.Top != pc.Stroke.Color { pc.Stroke.Color = bs.Color.Top } pc.Stroke.Width = bs.Width.Top pc.LineTo(xtri, ytr) if r.Right != 0 { pc.CircularArc(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.Draw() pc.MoveTo(xtr, ytri) } if bs.Color.Right != pc.Stroke.Color { pc.Stroke.Color = bs.Color.Right } pc.Stroke.Width = bs.Width.Right pc.LineTo(xbr, ybri) if r.Bottom != 0 { pc.CircularArc(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.Draw() pc.MoveTo(xbri, ybr) } if bs.Color.Bottom != pc.Stroke.Color { pc.Stroke.Color = bs.Color.Bottom } pc.Stroke.Width = bs.Width.Bottom pc.LineTo(xbli, ybl) if r.Left != 0 { pc.CircularArc(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.Draw() pc.MoveTo(xbl, ybli) } if bs.Color.Left != pc.Stroke.Color { pc.Stroke.Color = bs.Color.Left } pc.Stroke.Width = bs.Width.Left pc.LineTo(xtl, ytli) if r.Top != 0 { pc.CircularArc(xtli, ytli, r.Top, math32.DegToRad(180), math32.DegToRad(270)) } pc.LineTo(xtli, ytl) pc.Draw() } // RoundedShadowBlur 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 *Painter) RoundedShadowBlur(blurSigma, radiusFactor, x, y, w, h float32, r sides.Floats) { if blurSigma <= 0 || radiusFactor <= 0 { pc.RoundedRectangleSides(x, y, w, h, r) pc.Draw() 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.Stroke origFill := pc.Fill origOpacity := pc.Fill.Opacity pc.Stroke.Color = nil pc.RoundedRectangleSides(x+br, y+br, w-2*br, h-2*br, r) pc.Draw() pc.Stroke.Color = pc.Fill.Color pc.Fill.Color = nil pc.Stroke.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.Stroke.Opacity = b * origOpacity pc.RoundedRectangleSides(x+bo, y+bo, w-2*bo, h-2*bo, r) pc.Draw() } pc.Stroke = origStroke pc.Fill = origFill } //////// Image drawing // FillBox performs an optimized fill of the given // rectangular region with the given image. It is equivalent // to [Painter.DrawBox] with [draw.Over]. func (pc *Painter) 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. It is equivalent // to [Painter.DrawBox] with [draw.Src]. func (pc *Painter) 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. // If the image is nil, a new transparent color is used. func (pc *Painter) DrawBox(pos, size math32.Vector2, img image.Image, op draw.Op) { if img == nil { img = colors.Uniform(color.RGBA{}) } pos = pc.Transform().MulVector2AsPoint(pos) size = pc.Transform().MulVector2AsVector(size) br := math32.RectFromPosSizeMax(pos, size) cb := pc.Context().Bounds.Rect.ToRect() b := cb.Intersect(br) if b.Size() == (image.Point{}) { return } if g, ok := img.(gradient.Gradient); ok { g.Update(pc.Fill.Opacity, math32.B2FromRect(b), pc.Transform()) } else { img = gradient.ApplyOpacity(img, pc.Fill.Opacity) } pc.Render.Add(pimage.NewDraw(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 *Painter) BlurBox(pos, size math32.Vector2, blurRadius float32) { rect := math32.RectFromPosSizeMax(pos, size) pc.Render.Add(pimage.NewBlur(rect, blurRadius)) } // 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 *Painter) 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 *Painter) AsMask() *image.Alpha { // b := pc.Image.Bounds() // mask := image.NewAlpha(b) // draw.Draw(mask, b, pc.Image, image.Point{}, draw.Src) // return mask // } // Clear fills the entire image with the current fill color. func (pc *Painter) Clear() { src := pc.Fill.Color pc.Render.Add(pimage.NewClear(src, image.Point{}, draw.Src)) } // SetPixel sets the color of the specified pixel using the current stroke color. func (pc *Painter) SetPixel(x, y int) { pc.Render.Add(pimage.NewSetPixel(image.Point{x, y}, pc.Stroke.Color)) } // DrawImage draws the given image at the specified starting point, // using the bounds of the source image in rectangle rect, using // the given draw operration: Over = overlay (alpha blend with destination) // Src = copy source directly, overwriting destination pixels. func (pc *Painter) DrawImage(src image.Image, rect image.Rectangle, srcStart image.Point, op draw.Op) { pc.Render.Add(pimage.NewDraw(rect, src, srcStart, op)) } // DrawImageAnchored draws the specified image at the specified anchor point. // The anchor point is x - w * ax, y - h * ay, where w, h is the size of the // image. Use ax=0.5, ay=0.5 to center the image at the specified point. func (pc *Painter) DrawImageAnchored(src image.Image, x, y, ax, ay float32) { s := src.Bounds().Size() x -= ax * float32(s.X) y -= ay * float32(s.Y) m := pc.Transform().Translate(x, y) if pc.Mask == nil { pc.Render.Add(pimage.NewTransform(m, src.Bounds(), src, draw.Over)) } else { pc.Render.Add(pimage.NewTransformMask(m, src.Bounds(), src, draw.Over, pc.Mask, 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 *Painter) DrawImageScaled(src image.Image, x, y, w, h float32) { s := src.Bounds().Size() isz := math32.FromPoint(s) isc := math32.Vec2(w, h).Div(isz) m := pc.Transform().Translate(x, y).Scale(isc.X, isc.Y) if pc.Mask == nil { pc.Render.Add(pimage.NewTransform(m, src.Bounds(), src, draw.Over)) } else { pc.Render.Add(pimage.NewTransformMask(m, src.Bounds(), src, draw.Over, pc.Mask, image.Point{})) } } // BoundingBox computes the bounding box for an element in pixel int // coordinates, applying current transform func (pc *Painter) BoundingBox(minX, minY, maxX, maxY float32) image.Rectangle { sw := float32(0.0) // if pc.Stroke.Color != nil {// todo // sw = 0.5 * pc.StrokeWidth() // } tmin := pc.Transform().MulVector2AsPoint(math32.Vec2(minX, minY)) tmax := pc.Transform().MulVector2AsPoint(math32.Vec2(maxX, maxY)) 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 *Painter) 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) } /////// DrawText // DrawText adds given [shaped] text lines to the rendering list, // at given position. Note that all rendering is subject to the // current active transform, including the position: // e.g., use math32.Rotate2DAround to just rotate the text at a given // absolute position offset. func (pc *Painter) DrawText(tx *shaped.Lines, pos math32.Vector2) { pc.Render.Add(render.NewText(tx, pc.Paint, pc.Context(), 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. package pimage import ( "image" "math" "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 } // Code generated by "core generate"; DO NOT EDIT. package pimage import ( "cogentcore.org/core/enums" ) var _CmdsValues = []Cmds{0, 1, 2, 3} // CmdsN is the highest valid value for type Cmds, plus one. const CmdsN Cmds = 4 var _CmdsValueMap = map[string]Cmds{`Draw`: 0, `Transform`: 1, `Blur`: 2, `SetPixel`: 3} var _CmdsDescMap = map[Cmds]string{0: `Draw Source image using draw.Draw equivalent function, without any transformation. If Mask is non-nil it is used.`, 1: `Draw Source image with transform. If Mask is non-nil, it is used.`, 2: `blurs the Rect region with the given blur radius. The blur radius passed to this function is the actual Gaussian standard deviation (σ).`, 3: `Sets pixel from Source image at Pos`} var _CmdsMap = map[Cmds]string{0: `Draw`, 1: `Transform`, 2: `Blur`, 3: `SetPixel`} // String returns the string representation of this Cmds value. func (i Cmds) String() string { return enums.String(i, _CmdsMap) } // SetString sets the Cmds value from its string representation, // and returns an error if the string is invalid. func (i *Cmds) SetString(s string) error { return enums.SetString(i, s, _CmdsValueMap, "Cmds") } // Int64 returns the Cmds value as an int64. func (i Cmds) Int64() int64 { return int64(i) } // SetInt64 sets the Cmds value from an int64. func (i *Cmds) SetInt64(in int64) { *i = Cmds(in) } // Desc returns the description of the Cmds value. func (i Cmds) Desc() string { return enums.Desc(i, _CmdsDescMap) } // CmdsValues returns all possible values for the type Cmds. func CmdsValues() []Cmds { return _CmdsValues } // Values returns all possible values for the type Cmds. func (i Cmds) Values() []enums.Enum { return enums.Values(_CmdsValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Cmds) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Cmds) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Cmds") } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package pimage //go:generate core generate import ( "image" "cogentcore.org/core/base/iox/imagex" "cogentcore.org/core/math32" "golang.org/x/image/draw" "golang.org/x/image/math/f64" ) // Cmds are possible commands to perform for [Params]. type Cmds int32 //enums:enum const ( // Draw Source image using draw.Draw equivalent function, // without any transformation. If Mask is non-nil it is used. Draw Cmds = iota // Draw Source image with transform. If Mask is non-nil, it is used. Transform // blurs the Rect region with the given blur radius. // The blur radius passed to this function is the actual Gaussian // standard deviation (σ). Blur // Sets pixel from Source image at Pos SetPixel ) // Params for image operations. This is a Render Item. type Params struct { // Command to perform. Cmd Cmds // Rect is the rectangle to draw into. This is the bounds for Transform source. // If empty, the entire destination image Bounds() are used. Rect image.Rectangle // SourcePos is the position for the source image in Draw, // and the location for SetPixel. SourcePos image.Point // Draw operation: Src or Over Op draw.Op // Source to draw. Source image.Image // Mask, used if non-nil. Mask image.Image // MaskPos is the position for the mask MaskPos image.Point // Transform for image transform. Transform math32.Matrix2 // BlurRadius is the Gaussian standard deviation for Blur function BlurRadius float32 } func (pr *Params) IsRenderItem() {} // NewClear returns a new Clear that renders entire image with given source image. func NewClear(src image.Image, sp image.Point, op draw.Op) *Params { pr := &Params{Cmd: Draw, Rect: image.Rectangle{}, Source: imagex.WrapJS(src), SourcePos: sp, Op: op} return pr } // NewDraw returns a new Draw operation with given parameters. // Does nothing if rect is empty. func NewDraw(rect image.Rectangle, src image.Image, sp image.Point, op draw.Op) *Params { if rect == (image.Rectangle{}) { return nil } pr := &Params{Cmd: Draw, Rect: rect, Source: imagex.WrapJS(src), SourcePos: sp, Op: op} return pr } // NewDrawMask returns a new DrawMask operation with given parameters. // Does nothing if rect is empty. func NewDrawMask(rect image.Rectangle, src image.Image, sp image.Point, op draw.Op, mask image.Image, mp image.Point) *Params { if rect == (image.Rectangle{}) { return nil } pr := &Params{Cmd: Draw, Rect: rect, Source: imagex.WrapJS(src), SourcePos: sp, Op: op, Mask: imagex.WrapJS(mask), MaskPos: mp} return pr } // NewTransform returns a new Transform operation with given parameters. // Does nothing if rect is empty. func NewTransform(m math32.Matrix2, rect image.Rectangle, src image.Image, op draw.Op) *Params { if rect == (image.Rectangle{}) { return nil } pr := &Params{Cmd: Transform, Transform: m, Rect: rect, Source: imagex.WrapJS(src), Op: op} return pr } // NewTransformMask returns a new Transform Mask operation with given parameters. // Does nothing if rect is empty. func NewTransformMask(m math32.Matrix2, rect image.Rectangle, src image.Image, op draw.Op, mask image.Image, mp image.Point) *Params { if rect == (image.Rectangle{}) { return nil } pr := &Params{Cmd: Transform, Transform: m, Rect: rect, Source: imagex.WrapJS(src), Op: op, Mask: imagex.WrapJS(mask), MaskPos: mp} return pr } // NewBlur returns a new Blur operation with given parameters. // Does nothing if rect is empty. func NewBlur(rect image.Rectangle, blurRadius float32) *Params { if rect == (image.Rectangle{}) { return nil } pr := &Params{Cmd: Blur, Rect: rect, BlurRadius: blurRadius} return pr } // NewSetPixel returns a new SetPixel operation with given parameters. func NewSetPixel(at image.Point, clr image.Image) *Params { pr := &Params{Cmd: SetPixel, SourcePos: at, Source: clr} return pr } // Render performs the image operation on given destination image. func (pr *Params) Render(dest *image.RGBA) { switch pr.Cmd { case Draw: if pr.Rect == (image.Rectangle{}) { pr.Rect = dest.Bounds() } if pr.Mask != nil { draw.DrawMask(dest, pr.Rect, imagex.Unwrap(pr.Source), pr.SourcePos, imagex.Unwrap(pr.Mask), pr.MaskPos, pr.Op) } else { if pr.Source == nil { return } draw.Draw(dest, pr.Rect, imagex.Unwrap(pr.Source), pr.SourcePos, pr.Op) } case Transform: m := pr.Transform s2d := f64.Aff3{float64(m.XX), float64(m.XY), float64(m.X0), float64(m.YX), float64(m.YY), float64(m.Y0)} tdraw := draw.BiLinear if pr.Mask != nil { tdraw.Transform(dest, s2d, imagex.Unwrap(pr.Source), pr.Rect, pr.Op, &draw.Options{ DstMask: imagex.Unwrap(pr.Mask), DstMaskP: pr.MaskPos, }) } else { tdraw.Transform(dest, s2d, imagex.Unwrap(pr.Source), pr.Rect, pr.Op, nil) } case Blur: sub := dest.SubImage(pr.Rect) sub = GaussianBlur(sub, float64(pr.BlurRadius)) draw.Draw(dest, pr.Rect, sub, pr.Rect.Min, draw.Src) case SetPixel: x := pr.SourcePos.X y := pr.SourcePos.Y dest.Set(x, y, imagex.Unwrap(pr.Source).At(x, y)) } } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import "cogentcore.org/core/math32" func QuadraticToCubicBezier(p0, p1, p2 math32.Vector2) (math32.Vector2, math32.Vector2) { c1 := p0.Lerp(p1, 2.0/3.0) c2 := p2.Lerp(p1, 2.0/3.0) return c1, c2 } func QuadraticBezierDeriv(p0, p1, p2 math32.Vector2, t float32) math32.Vector2 { p0 = p0.MulScalar(-2.0 + 2.0*t) p1 = p1.MulScalar(2.0 - 4.0*t) p2 = p2.MulScalar(2.0 * t) return p0.Add(p1).Add(p2) } func CubicBezierDeriv(p0, p1, p2, p3 math32.Vector2, t float32) math32.Vector2 { p0 = p0.MulScalar(-3.0 + 6.0*t - 3.0*t*t) p1 = p1.MulScalar(3.0 - 12.0*t + 9.0*t*t) p2 = p2.MulScalar(6.0*t - 9.0*t*t) p3 = p3.MulScalar(3.0 * t * t) return p0.Add(p1).Add(p2).Add(p3) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import "cogentcore.org/core/math32" // FastBounds returns the maximum bounding box rectangle of the path. // It is quicker than Bounds but less accurate. func (p Path) FastBounds() math32.Box2 { if len(p) < 4 { return math32.Box2{} } // first command is MoveTo start, end := math32.Vec2(p[1], p[2]), math32.Vector2{} xmin, xmax := start.X, start.X ymin, ymax := start.Y, start.Y for i := 4; i < len(p); { cmd := p[i] switch cmd { case MoveTo, LineTo, Close: end = math32.Vec2(p[i+1], p[i+2]) xmin = math32.Min(xmin, end.X) xmax = math32.Max(xmax, end.X) ymin = math32.Min(ymin, end.Y) ymax = math32.Max(ymax, end.Y) case QuadTo: cp := math32.Vec2(p[i+1], p[i+2]) end = math32.Vec2(p[i+3], p[i+4]) xmin = math32.Min(xmin, math32.Min(cp.X, end.X)) xmax = math32.Max(xmax, math32.Max(cp.X, end.X)) ymin = math32.Min(ymin, math32.Min(cp.Y, end.Y)) ymax = math32.Max(ymax, math32.Max(cp.Y, end.Y)) case CubeTo: cp1 := math32.Vec2(p[i+1], p[i+2]) cp2 := math32.Vec2(p[i+3], p[i+4]) end = math32.Vec2(p[i+5], p[i+6]) xmin = math32.Min(xmin, math32.Min(cp1.X, math32.Min(cp2.X, end.X))) xmax = math32.Max(xmax, math32.Max(cp1.X, math32.Min(cp2.X, end.X))) ymin = math32.Min(ymin, math32.Min(cp1.Y, math32.Min(cp2.Y, end.Y))) ymax = math32.Max(ymax, math32.Max(cp1.Y, math32.Min(cp2.Y, end.Y))) case ArcTo: var rx, ry, phi float32 var large, sweep bool rx, ry, phi, large, sweep, end = p.ArcToPoints(i) cx, cy, _, _ := EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) r := math32.Max(rx, ry) xmin = math32.Min(xmin, cx-r) xmax = math32.Max(xmax, cx+r) ymin = math32.Min(ymin, cy-r) ymax = math32.Max(ymax, cy+r) } i += CmdLen(cmd) start = end } return math32.B2(xmin, ymin, xmax, ymax) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import ( "cogentcore.org/core/math32" ) func EllipseDeriv(rx, ry, phi float32, sweep bool, theta float32) math32.Vector2 { sintheta, costheta := math32.Sincos(theta) sinphi, cosphi := math32.Sincos(phi) dx := -rx*sintheta*cosphi - ry*costheta*sinphi dy := -rx*sintheta*sinphi + ry*costheta*cosphi if !sweep { return math32.Vector2{-dx, -dy} } return math32.Vector2{dx, dy} } // EllipsePos adds the position on the ellipse at angle theta. func EllipsePos(rx, ry, phi, cx, cy, theta float32) math32.Vector2 { sintheta, costheta := math32.Sincos(theta) sinphi, cosphi := math32.Sincos(phi) x := cx + rx*costheta*cosphi - ry*sintheta*sinphi y := cy + rx*costheta*sinphi + ry*sintheta*cosphi return math32.Vector2{x, y} } // EllipseToCenter converts to the center arc format and returns // (centerX, centerY, angleFrom, angleTo) with angles in radians. // When angleFrom with range [0, 2*PI) is bigger than angleTo with range // (-2*PI, 4*PI), the ellipse runs clockwise. // The angles are from before the ellipse has been stretched and rotated. // See https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes func EllipseToCenter(x1, y1, rx, ry, phi float32, large, sweep bool, x2, y2 float32) (float32, float32, float32, float32) { if Equal(x1, x2) && Equal(y1, y2) { return x1, y1, 0.0, 0.0 } else if Equal(math32.Abs(x2-x1), rx) && Equal(y1, y2) && Equal(phi, 0.0) { // common case since circles are defined as two arcs from (+dx,0) to (-dx,0) and back cx, cy := x1+(x2-x1)/2.0, y1 theta := float32(0.0) if x1 < x2 { theta = math32.Pi } delta := float32(math32.Pi) if !sweep { delta = -delta } return cx, cy, theta, theta + delta } // compute the half distance between start and end point for the unrotated ellipse sinphi, cosphi := math32.Sincos(phi) x1p := cosphi*(x1-x2)/2.0 + sinphi*(y1-y2)/2.0 y1p := -sinphi*(x1-x2)/2.0 + cosphi*(y1-y2)/2.0 // check that radii are large enough to reduce rounding errors radiiCheck := x1p*x1p/rx/rx + y1p*y1p/ry/ry if 1.0 < radiiCheck { radiiScale := math32.Sqrt(radiiCheck) rx *= radiiScale ry *= radiiScale } // calculate the center point (cx,cy) sq := (rx*rx*ry*ry - rx*rx*y1p*y1p - ry*ry*x1p*x1p) / (rx*rx*y1p*y1p + ry*ry*x1p*x1p) if sq <= Epsilon { // Epsilon instead of 0.0 improves numerical stability for coef near zero // this happens when start and end points are at two opposites of the ellipse and // the line between them passes through the center, a common case sq = 0.0 } coef := math32.Sqrt(sq) if large == sweep { coef = -coef } cxp := coef * rx * y1p / ry cyp := coef * -ry * x1p / rx cx := cosphi*cxp - sinphi*cyp + (x1+x2)/2.0 cy := sinphi*cxp + cosphi*cyp + (y1+y2)/2.0 // specify U and V vectors; theta = arccos(U*V / sqrt(U*U + V*V)) ux := (x1p - cxp) / rx uy := (y1p - cyp) / ry vx := -(x1p + cxp) / rx vy := -(y1p + cyp) / ry // calculate the start angle (theta) and extent angle (delta) theta := math32.Acos(ux / math32.Sqrt(ux*ux+uy*uy)) if uy < 0.0 { theta = -theta } theta = AngleNorm(theta) deltaAcos := (ux*vx + uy*vy) / math32.Sqrt((ux*ux+uy*uy)*(vx*vx+vy*vy)) deltaAcos = math32.Min(1.0, math32.Max(-1.0, deltaAcos)) delta := math32.Acos(deltaAcos) if ux*vy-uy*vx < 0.0 { delta = -delta } if !sweep && 0.0 < delta { // clockwise in Cartesian delta -= 2.0 * math32.Pi } else if sweep && delta < 0.0 { // counter clockwise in Cartesian delta += 2.0 * math32.Pi } return cx, cy, theta, theta + delta } // scale ellipse if rx and ry are too small, see https://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii func EllipseRadiiCorrection(start math32.Vector2, rx, ry, phi float32, end math32.Vector2) float32 { diff := start.Sub(end) sinphi, cosphi := math32.Sincos(phi) x1p := (cosphi*diff.X + sinphi*diff.Y) / 2.0 y1p := (-sinphi*diff.X + cosphi*diff.Y) / 2.0 return math32.Sqrt(x1p*x1p/rx/rx + y1p*y1p/ry/ry) } // see Drawing and elliptical arc using polylines, quadratic or cubic Bézier curves (2003), L. Maisonobe, https://spaceroots.org/documents/ellipse/elliptical-arc.pdf func ellipseToCubicBeziers(start math32.Vector2, rx, ry, phi float32, large, sweep bool, end math32.Vector2) [][4]math32.Vector2 { cx, cy, theta0, theta1 := EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) dtheta := float32(math32.Pi / 2.0) // TODO: use error measure to determine dtheta? n := int(math32.Ceil(math32.Abs(theta1-theta0) / dtheta)) dtheta = math32.Abs(theta1-theta0) / float32(n) // evenly spread the n points, dalpha will get smaller kappa := math32.Sin(dtheta) * (math32.Sqrt(4.0+3.0*math32.Pow(math32.Tan(dtheta/2.0), 2.0)) - 1.0) / 3.0 if !sweep { dtheta = -dtheta } beziers := [][4]math32.Vector2{} startDeriv := EllipseDeriv(rx, ry, phi, sweep, theta0) for i := 1; i < n+1; i++ { theta := theta0 + float32(i)*dtheta end := EllipsePos(rx, ry, phi, cx, cy, theta) endDeriv := EllipseDeriv(rx, ry, phi, sweep, theta) cp1 := start.Add(startDeriv.MulScalar(kappa)) cp2 := end.Sub(endDeriv.MulScalar(kappa)) beziers = append(beziers, [4]math32.Vector2{start, cp1, cp2, end}) startDeriv = endDeriv start = end } return beziers } // see Drawing and elliptical arc using polylines, quadratic or cubic Bézier curves (2003), L. Maisonobe, https://spaceroots.org/documents/ellipse/elliptical-arc.pdf func ellipseToQuadraticBeziers(start math32.Vector2, rx, ry, phi float32, large, sweep bool, end math32.Vector2) [][3]math32.Vector2 { cx, cy, theta0, theta1 := EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) dtheta := float32(math32.Pi / 2.0) // TODO: use error measure to determine dtheta? n := int(math32.Ceil(math32.Abs(theta1-theta0) / dtheta)) dtheta = math32.Abs(theta1-theta0) / float32(n) // evenly spread the n points, dalpha will get smaller kappa := math32.Tan(dtheta / 2.0) if !sweep { dtheta = -dtheta } beziers := [][3]math32.Vector2{} startDeriv := EllipseDeriv(rx, ry, phi, sweep, theta0) for i := 1; i < n+1; i++ { theta := theta0 + float32(i)*dtheta end := EllipsePos(rx, ry, phi, cx, cy, theta) endDeriv := EllipseDeriv(rx, ry, phi, sweep, theta) cp := start.Add(startDeriv.MulScalar(kappa)) beziers = append(beziers, [3]math32.Vector2{start, cp, end}) startDeriv = endDeriv start = end } return beziers } // Code generated by "core generate"; DO NOT EDIT. package ppath import ( "cogentcore.org/core/enums" ) var _FillRulesValues = []FillRules{0, 1, 2, 3} // FillRulesN is the highest valid value for type FillRules, plus one. const FillRulesN FillRules = 4 var _FillRulesValueMap = map[string]FillRules{`nonzero`: 0, `evenodd`: 1, `positive`: 2, `negative`: 3} var _FillRulesDescMap = map[FillRules]string{0: ``, 1: ``, 2: ``, 3: ``} var _FillRulesMap = map[FillRules]string{0: `nonzero`, 1: `evenodd`, 2: `positive`, 3: `negative`} // 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 _CapsValues = []Caps{0, 1, 2} // CapsN is the highest valid value for type Caps, plus one. const CapsN Caps = 3 var _CapsValueMap = map[string]Caps{`butt`: 0, `round`: 1, `square`: 2} var _CapsDescMap = map[Caps]string{0: `CapButt indicates to draw no line caps; it draws a line with the length of the specified length.`, 1: `CapRound indicates to draw a semicircle on each line end with a diameter of the stroke width.`, 2: `CapSquare 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.`} var _CapsMap = map[Caps]string{0: `butt`, 1: `round`, 2: `square`} // String returns the string representation of this Caps value. func (i Caps) String() string { return enums.String(i, _CapsMap) } // SetString sets the Caps value from its string representation, // and returns an error if the string is invalid. func (i *Caps) SetString(s string) error { return enums.SetString(i, s, _CapsValueMap, "Caps") } // Int64 returns the Caps value as an int64. func (i Caps) Int64() int64 { return int64(i) } // SetInt64 sets the Caps value from an int64. func (i *Caps) SetInt64(in int64) { *i = Caps(in) } // Desc returns the description of the Caps value. func (i Caps) Desc() string { return enums.Desc(i, _CapsDescMap) } // CapsValues returns all possible values for the type Caps. func CapsValues() []Caps { return _CapsValues } // Values returns all possible values for the type Caps. func (i Caps) Values() []enums.Enum { return enums.Values(_CapsValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Caps) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Caps) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Caps") } var _JoinsValues = []Joins{0, 1, 2, 3, 4, 5} // JoinsN is the highest valid value for type Joins, plus one. const JoinsN Joins = 6 var _JoinsValueMap = map[string]Joins{`miter`: 0, `miter-clip`: 1, `round`: 2, `bevel`: 3, `arcs`: 4, `arcs-clip`: 5} var _JoinsDescMap = map[Joins]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``} var _JoinsMap = map[Joins]string{0: `miter`, 1: `miter-clip`, 2: `round`, 3: `bevel`, 4: `arcs`, 5: `arcs-clip`} // String returns the string representation of this Joins value. func (i Joins) String() string { return enums.String(i, _JoinsMap) } // SetString sets the Joins value from its string representation, // and returns an error if the string is invalid. func (i *Joins) SetString(s string) error { return enums.SetString(i, s, _JoinsValueMap, "Joins") } // Int64 returns the Joins value as an int64. func (i Joins) Int64() int64 { return int64(i) } // SetInt64 sets the Joins value from an int64. func (i *Joins) SetInt64(in int64) { *i = Joins(in) } // Desc returns the description of the Joins value. func (i Joins) Desc() string { return enums.Desc(i, _JoinsDescMap) } // JoinsValues returns all possible values for the type Joins. func JoinsValues() []Joins { return _JoinsValues } // Values returns all possible values for the type Joins. func (i Joins) Values() []enums.Enum { return enums.Values(_JoinsValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Joins) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Joins) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Joins") } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package intersect import ( "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" ) func cubicBezierDeriv2(p0, p1, p2, p3 math32.Vector2, t float32) math32.Vector2 { p0 = p0.MulScalar(6.0 - 6.0*t) p1 = p1.MulScalar(18.0*t - 12.0) p2 = p2.MulScalar(6.0 - 18.0*t) p3 = p3.MulScalar(6.0 * t) return p0.Add(p1).Add(p2).Add(p3) } func cubicBezierDeriv3(p0, p1, p2, p3 math32.Vector2, t float32) math32.Vector2 { p0 = p0.MulScalar(-6.0) p1 = p1.MulScalar(18.0) p2 = p2.MulScalar(-18.0) p3 = p3.MulScalar(6.0) return p0.Add(p1).Add(p2).Add(p3) } func cubicBezierPos(p0, p1, p2, p3 math32.Vector2, t float32) math32.Vector2 { p0 = p0.MulScalar(1.0 - 3.0*t + 3.0*t*t - t*t*t) p1 = p1.MulScalar(3.0*t - 6.0*t*t + 3.0*t*t*t) p2 = p2.MulScalar(3.0*t*t - 3.0*t*t*t) p3 = p3.MulScalar(t * t * t) return p0.Add(p1).Add(p2).Add(p3) } func quadraticBezierDeriv2(p0, p1, p2 math32.Vector2) math32.Vector2 { p0 = p0.MulScalar(2.0) p1 = p1.MulScalar(-4.0) p2 = p2.MulScalar(2.0) return p0.Add(p1).Add(p2) } func quadraticBezierPos(p0, p1, p2 math32.Vector2, t float32) math32.Vector2 { p0 = p0.MulScalar(1.0 - 2.0*t + t*t) p1 = p1.MulScalar(2.0*t - 2.0*t*t) p2 = p2.MulScalar(t * t) return p0.Add(p1).Add(p2) } // negative when curve bends CW while following t func CubicBezierCurvatureRadius(p0, p1, p2, p3 math32.Vector2, t float32) float32 { dp := ppath.CubicBezierDeriv(p0, p1, p2, p3, t) ddp := cubicBezierDeriv2(p0, p1, p2, p3, t) a := dp.Cross(ddp) // negative when bending right ie. curve is CW at this point if ppath.Equal(a, 0.0) { return math32.NaN() } return math32.Pow(dp.X*dp.X+dp.Y*dp.Y, 1.5) / a } // negative when curve bends CW while following t func quadraticBezierCurvatureRadius(p0, p1, p2 math32.Vector2, t float32) float32 { dp := ppath.QuadraticBezierDeriv(p0, p1, p2, t) ddp := quadraticBezierDeriv2(p0, p1, p2) a := dp.Cross(ddp) // negative when bending right ie. curve is CW at this point if ppath.Equal(a, 0.0) { return math32.NaN() } return math32.Pow(dp.X*dp.X+dp.Y*dp.Y, 1.5) / a } // see https://malczak.linuxpl.com/blog/quadratic-bezier-curve-length/ func quadraticBezierLength(p0, p1, p2 math32.Vector2) float32 { a := p0.Sub(p1.MulScalar(2.0)).Add(p2) b := p1.MulScalar(2.0).Sub(p0.MulScalar(2.0)) A := 4.0 * a.Dot(a) B := 4.0 * a.Dot(b) C := b.Dot(b) if ppath.Equal(A, 0.0) { // p1 is in the middle between p0 and p2, so it is a straight line from p0 to p2 return p2.Sub(p0).Length() } Sabc := 2.0 * math32.Sqrt(A+B+C) A2 := math32.Sqrt(A) A32 := 2.0 * A * A2 C2 := 2.0 * math32.Sqrt(C) BA := B / A2 return (A32*Sabc + A2*B*(Sabc-C2) + (4.0*C*A-B*B)*math32.Log((2.0*A2+BA+Sabc)/(BA+C2))) / (4.0 * A32) } func findInflectionPointCubicBezier(p0, p1, p2, p3 math32.Vector2) (float32, float32) { // see www.faculty.idc.ac.il/arik/quality/appendixa.html // we omit multiplying bx,by,cx,cy with 3.0, so there is no need for divisions when calculating a,b,c ax := -p0.X + 3.0*p1.X - 3.0*p2.X + p3.X ay := -p0.Y + 3.0*p1.Y - 3.0*p2.Y + p3.Y bx := p0.X - 2.0*p1.X + p2.X by := p0.Y - 2.0*p1.Y + p2.Y cx := -p0.X + p1.X cy := -p0.Y + p1.Y a := (ay*bx - ax*by) b := (ay*cx - ax*cy) c := (by*cx - bx*cy) x1, x2 := solveQuadraticFormula(a, b, c) if x1 < ppath.Epsilon/2.0 || 1.0-ppath.Epsilon/2.0 < x1 { x1 = math32.NaN() } if x2 < ppath.Epsilon/2.0 || 1.0-ppath.Epsilon/2.0 < x2 { x2 = math32.NaN() } else if math32.IsNaN(x1) { x1, x2 = x2, x1 } return x1, x2 } func findInflectionPointRangeCubicBezier(p0, p1, p2, p3 math32.Vector2, t, tolerance float32) (float32, float32) { // find the range around an inflection point that we consider flat within the flatness criterion if math32.IsNaN(t) { return math32.Inf(1), math32.Inf(1) } if t < 0.0 || t > 1.0 { panic("t outside 0.0--1.0 range") } // we state that s(t) = 3*s2*t^2 + (s3 - 3*s2)*t^3 (see paper on the r-s coordinate system) // with s(t) aligned perpendicular to the curve at t = 0 // then we impose that s(tf) = flatness and find tf // at inflection points however, s2 = 0, so that s(t) = s3*t^3 if !ppath.Equal(t, 0.0) { _, _, _, _, p0, p1, p2, p3 = cubicBezierSplit(p0, p1, p2, p3, t) } nr := p1.Sub(p0) ns := p3.Sub(p0) if ppath.Equal(nr.X, 0.0) && ppath.Equal(nr.Y, 0.0) { // if p0=p1, then rn (the velocity at t=0) needs adjustment // nr = lim[t->0](B'(t)) = 3*(p1-p0) + 6*t*((p1-p0)+(p2-p1)) + second order terms of t // if (p1-p0)->0, we use (p2-p1)=(p2-p0) nr = p2.Sub(p0) } if ppath.Equal(nr.X, 0.0) && ppath.Equal(nr.Y, 0.0) { // if rn is still zero, this curve has p0=p1=p2, so it is straight return 0.0, 1.0 } s3 := math32.Abs(ns.X*nr.Y-ns.Y*nr.X) / math32.Hypot(nr.X, nr.Y) if ppath.Equal(s3, 0.0) { return 0.0, 1.0 // can approximate whole curve linearly } tf := math32.Cbrt(tolerance / s3) return t - tf*(1.0-t), t + tf*(1.0-t) } // cubicBezierLength calculates the length of the Bézier, taking care of inflection points. It uses Gauss-Legendre (n=5) and has an error of ~1% or less (empirical). func cubicBezierLength(p0, p1, p2, p3 math32.Vector2) float32 { t1, t2 := findInflectionPointCubicBezier(p0, p1, p2, p3) var beziers [][4]math32.Vector2 if t1 > 0.0 && t1 < 1.0 && t2 > 0.0 && t2 < 1.0 { p0, p1, p2, p3, q0, q1, q2, q3 := cubicBezierSplit(p0, p1, p2, p3, t1) t2 = (t2 - t1) / (1.0 - t1) q0, q1, q2, q3, r0, r1, r2, r3 := cubicBezierSplit(q0, q1, q2, q3, t2) beziers = append(beziers, [4]math32.Vector2{p0, p1, p2, p3}) beziers = append(beziers, [4]math32.Vector2{q0, q1, q2, q3}) beziers = append(beziers, [4]math32.Vector2{r0, r1, r2, r3}) } else if t1 > 0.0 && t1 < 1.0 { p0, p1, p2, p3, q0, q1, q2, q3 := cubicBezierSplit(p0, p1, p2, p3, t1) beziers = append(beziers, [4]math32.Vector2{p0, p1, p2, p3}) beziers = append(beziers, [4]math32.Vector2{q0, q1, q2, q3}) } else { beziers = append(beziers, [4]math32.Vector2{p0, p1, p2, p3}) } length := float32(0.0) for _, bezier := range beziers { speed := func(t float32) float32 { return ppath.CubicBezierDeriv(bezier[0], bezier[1], bezier[2], bezier[3], t).Length() } length += gaussLegendre7(speed, 0.0, 1.0) } return length } func quadraticBezierSplit(p0, p1, p2 math32.Vector2, t float32) (math32.Vector2, math32.Vector2, math32.Vector2, math32.Vector2, math32.Vector2, math32.Vector2) { q0 := p0 q1 := p0.Lerp(p1, t) r2 := p2 r1 := p1.Lerp(p2, t) r0 := q1.Lerp(r1, t) q2 := r0 return q0, q1, q2, r0, r1, r2 } func cubicBezierSplit(p0, p1, p2, p3 math32.Vector2, t float32) (math32.Vector2, math32.Vector2, math32.Vector2, math32.Vector2, math32.Vector2, math32.Vector2, math32.Vector2, math32.Vector2) { pm := p1.Lerp(p2, t) q0 := p0 q1 := p0.Lerp(p1, t) q2 := q1.Lerp(pm, t) r3 := p3 r2 := p2.Lerp(p3, t) r1 := pm.Lerp(r2, t) r0 := q2.Lerp(r1, t) q3 := r0 return q0, q1, q2, q3, r0, r1, r2, r3 } func cubicBezierNumInflections(p0, p1, p2, p3 math32.Vector2) int { t1, t2 := findInflectionPointCubicBezier(p0, p1, p2, p3) if !math32.IsNaN(t2) { return 2 } else if !math32.IsNaN(t1) { return 1 } return 0 } func xmonotoneCubicBezier(p0, p1, p2, p3 math32.Vector2) ppath.Path { a := -p0.X + 3*p1.X - 3*p2.X + p3.X b := 2*p0.X - 4*p1.X + 2*p2.X c := -p0.X + p1.X p := ppath.Path{} p.MoveTo(p0.X, p0.Y) split := false t1, t2 := solveQuadraticFormula(a, b, c) if !math32.IsNaN(t1) && inIntervalExclusive(t1, 0.0, 1.0) { _, q1, q2, q3, r0, r1, r2, r3 := cubicBezierSplit(p0, p1, p2, p3, t1) p.CubeTo(q1.X, q1.Y, q2.X, q2.Y, q3.X, q3.Y) p0, p1, p2, p3 = r0, r1, r2, r3 split = true } if !math32.IsNaN(t2) && inIntervalExclusive(t2, 0.0, 1.0) { if split { t2 = (t2 - t1) / (1.0 - t1) } _, q1, q2, q3, _, r1, r2, r3 := cubicBezierSplit(p0, p1, p2, p3, t2) p.CubeTo(q1.X, q1.Y, q2.X, q2.Y, q3.X, q3.Y) p1, p2, p3 = r1, r2, r3 } p.CubeTo(p1.X, p1.Y, p2.X, p2.Y, p3.X, p3.Y) return p } func quadraticBezierDistance(p0, p1, p2, q math32.Vector2) float32 { f := p0.Sub(p1.MulScalar(2.0)).Add(p2) g := p1.MulScalar(2.0).Sub(p0.MulScalar(2.0)) h := p0.Sub(q) a := 4.0 * (f.X*f.X + f.Y*f.Y) b := 6.0 * (f.X*g.X + f.Y*g.Y) c := 2.0 * (2.0*(f.X*h.X+f.Y*h.Y) + g.X*g.X + g.Y*g.Y) d := 2.0 * (g.X*h.X + g.Y*h.Y) dist := math32.Inf(1.0) t0, t1, t2 := solveCubicFormula(a, b, c, d) ts := []float32{t0, t1, t2, 0.0, 1.0} for _, t := range ts { if !math32.IsNaN(t) { if t < 0.0 { t = 0.0 } else if 1.0 < t { t = 1.0 } if tmpDist := quadraticBezierPos(p0, p1, p2, t).Sub(q).Length(); tmpDist < dist { dist = tmpDist } } } return dist } func xmonotoneQuadraticBezier(p0, p1, p2 math32.Vector2) ppath.Path { p := ppath.Path{} p.MoveTo(p0.X, p0.Y) if tdenom := (p0.X - 2*p1.X + p2.X); !ppath.Equal(tdenom, 0.0) { if t := (p0.X - p1.X) / tdenom; 0.0 < t && t < 1.0 { _, q1, q2, _, r1, r2 := quadraticBezierSplit(p0, p1, p2, t) p.QuadTo(q1.X, q1.Y, q2.X, q2.Y) p1, p2 = r1, r2 } } p.QuadTo(p1.X, p1.Y, p2.X, p2.Y) return p } // return the normal at the right-side of the curve (when increasing t) func CubicBezierNormal(p0, p1, p2, p3 math32.Vector2, t, d float32) math32.Vector2 { // TODO: remove and use CubicBezierDeriv + Rot90CW? if t == 0.0 { n := p1.Sub(p0) if n.X == 0 && n.Y == 0 { n = p2.Sub(p0) } if n.X == 0 && n.Y == 0 { n = p3.Sub(p0) } if n.X == 0 && n.Y == 0 { return math32.Vector2{} } return n.Rot90CW().Normal().MulScalar(d) } else if t == 1.0 { n := p3.Sub(p2) if n.X == 0 && n.Y == 0 { n = p3.Sub(p1) } if n.X == 0 && n.Y == 0 { n = p3.Sub(p0) } if n.X == 0 && n.Y == 0 { return math32.Vector2{} } return n.Rot90CW().Normal().MulScalar(d) } panic("not implemented") // not needed } func addCubicBezierLine(p *ppath.Path, p0, p1, p2, p3 math32.Vector2, t, d float32) { if ppath.EqualPoint(p0, p3) && (ppath.EqualPoint(p0, p1) || ppath.EqualPoint(p0, p2)) { // Bézier has p0=p1=p3 or p0=p2=p3 and thus has no surface or length return } pos := math32.Vector2{} if t == 0.0 { // line to beginning of path pos = p0 if d != 0.0 { n := CubicBezierNormal(p0, p1, p2, p3, t, d) pos = pos.Add(n) } } else if t == 1.0 { // line to the end of the path pos = p3 if d != 0.0 { n := CubicBezierNormal(p0, p1, p2, p3, t, d) pos = pos.Add(n) } } else { panic("not implemented") } p.LineTo(pos.X, pos.Y) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package intersect import ( "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" ) func ellipseDeriv2(rx, ry, phi float32, theta float32) math32.Vector2 { sintheta, costheta := math32.Sincos(theta) sinphi, cosphi := math32.Sincos(phi) ddx := -rx*costheta*cosphi + ry*sintheta*sinphi ddy := -rx*costheta*sinphi - ry*sintheta*cosphi return math32.Vector2{ddx, ddy} } // ellipseLength calculates the length of the elliptical arc // it uses Gauss-Legendre (n=5) and has an error of ~1% or less (empirical) func ellipseLength(rx, ry, theta1, theta2 float32) float32 { if theta2 < theta1 { theta1, theta2 = theta2, theta1 } speed := func(theta float32) float32 { return ppath.EllipseDeriv(rx, ry, 0.0, true, theta).Length() } return gaussLegendre5(speed, theta1, theta2) } func EllipseCurvatureRadius(rx, ry float32, sweep bool, theta float32) float32 { // positive for ccw / sweep // phi has no influence on the curvature dp := ppath.EllipseDeriv(rx, ry, 0.0, sweep, theta) ddp := ellipseDeriv2(rx, ry, 0.0, theta) a := dp.Cross(ddp) if ppath.Equal(a, 0.0) { return math32.NaN() } return math32.Pow(dp.X*dp.X+dp.Y*dp.Y, 1.5) / a } // ellipseSplit returns the new mid point, the two large parameters and the ok bool, the rest stays the same func ellipseSplit(rx, ry, phi, cx, cy, theta0, theta1, theta float32) (math32.Vector2, bool, bool, bool) { if !ppath.IsAngleBetween(theta, theta0, theta1) { return math32.Vector2{}, false, false, false } mid := ppath.EllipsePos(rx, ry, phi, cx, cy, theta) large0, large1 := false, false if math32.Abs(theta-theta0) > math32.Pi { large0 = true } else if math32.Abs(theta-theta1) > math32.Pi { large1 = true } return mid, large0, large1, true } func xmonotoneEllipticArc(start math32.Vector2, rx, ry, phi float32, large, sweep bool, end math32.Vector2) ppath.Path { sign := float32(1.0) if !sweep { sign = -1.0 } cx, cy, theta0, theta1 := ppath.EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) sinphi, cosphi := math32.Sincos(phi) thetaRight := math32.Atan2(-ry*sinphi, rx*cosphi) thetaLeft := thetaRight + math32.Pi p := ppath.Path{} p.MoveTo(start.X, start.Y) left := !ppath.AngleEqual(thetaLeft, theta0) && ppath.AngleNorm(sign*(thetaLeft-theta0)) < ppath.AngleNorm(sign*(thetaRight-theta0)) for t := theta0; !ppath.AngleEqual(t, theta1); { dt := ppath.AngleNorm(sign * (theta1 - t)) if left { dt = math32.Min(dt, ppath.AngleNorm(sign*(thetaLeft-t))) } else { dt = math32.Min(dt, ppath.AngleNorm(sign*(thetaRight-t))) } t += sign * dt pos := ppath.EllipsePos(rx, ry, phi, cx, cy, t) p.ArcTo(rx, ry, phi, false, sweep, pos.X, pos.Y) left = !left } return p } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package intersect import ( "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" ) // Flatten flattens all Bézier and arc curves into linear segments // and returns a new path. It uses tolerance as the maximum deviation. func Flatten(p ppath.Path, tolerance float32) ppath.Path { quad := func(p0, p1, p2 math32.Vector2) ppath.Path { return FlattenQuadraticBezier(p0, p1, p2, tolerance) } cube := func(p0, p1, p2, p3 math32.Vector2) ppath.Path { return FlattenCubicBezier(p0, p1, p2, p3, 0.0, tolerance) } arc := func(start math32.Vector2, rx, ry, phi float32, large, sweep bool, end math32.Vector2) ppath.Path { return FlattenEllipticArc(start, rx, ry, phi, large, sweep, end, tolerance) } return p.Replace(nil, quad, cube, arc) } func FlattenEllipticArc(start math32.Vector2, rx, ry, phi float32, large, sweep bool, end math32.Vector2, tolerance float32) ppath.Path { if ppath.Equal(rx, ry) { // circle r := rx cx, cy, theta0, theta1 := ppath.EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) theta0 += phi theta1 += phi // draw line segments from arc+tolerance to arc+tolerance, touching arc-tolerance in between // we start and end at the arc itself dtheta := math32.Abs(theta1 - theta0) thetaEnd := math32.Acos((r - tolerance) / r) // half angle of first/last segment thetaMid := math32.Acos((r - tolerance) / (r + tolerance)) // half angle of middle segments n := math32.Ceil((dtheta - thetaEnd*2.0) / (thetaMid * 2.0)) // evenly space out points along arc ratio := dtheta / (thetaEnd*2.0 + thetaMid*2.0*n) thetaEnd *= ratio thetaMid *= ratio // adjust distance from arc to lower total deviation area, add points on the outer circle // of the tolerance since the middle of the line segment touches the inner circle and thus // even out. Ratio < 1 is when the line segments are shorter (and thus not touch the inner // tolerance circle). r += ratio * tolerance p := ppath.Path{} p.MoveTo(start.X, start.Y) theta := thetaEnd + thetaMid for i := 0; i < int(n); i++ { t := theta0 + math32.Copysign(theta, theta1-theta0) pos := math32.Vector2Polar(t, r).Add(math32.Vector2{cx, cy}) p.LineTo(pos.X, pos.Y) theta += 2.0 * thetaMid } p.LineTo(end.X, end.Y) return p } // TODO: (flatten ellipse) use direct algorithm return Flatten(ppath.ArcToCube(start, rx, ry, phi, large, sweep, end), tolerance) } func FlattenQuadraticBezier(p0, p1, p2 math32.Vector2, tolerance float32) ppath.Path { // see Flat, precise flattening of cubic Bézier path and offset curves, by T.F. Hain et al., 2005, https://www.sciencedirect.com/science/article/pii/S0097849305001287 t := float32(0.0) p := ppath.Path{} p.MoveTo(p0.X, p0.Y) for t < 1.0 { D := p1.Sub(p0) if ppath.EqualPoint(p0, p1) { // p0 == p1, curve is a straight line from p0 to p2 // should not occur directly from paths as this is prevented in QuadTo, but may appear in other subroutines break } denom := math32.Hypot(D.X, D.Y) // equal to r1 s2nom := D.Cross(p2.Sub(p0)) //effFlatness := tolerance / (1.0 - d*s2nom/(denom*denom*denom)/2.0) t = 2.0 * math32.Sqrt(tolerance*math32.Abs(denom/s2nom)) if t >= 1.0 { break } _, _, _, p0, p1, p2 = quadraticBezierSplit(p0, p1, p2, t) p.LineTo(p0.X, p0.Y) } p.LineTo(p2.X, p2.Y) return p } // see Flat, precise flattening of cubic Bézier path and offset curves, by T.F. Hain et al., 2005, https://www.sciencedirect.com/science/article/pii/S0097849305001287 // see https://github.com/Manishearth/stylo-flat/blob/master/gfx/2d/Path.cpp for an example implementation // or https://docs.rs/crate/lyon_bezier/0.4.1/source/src/flatten_cubic.rs // p0, p1, p2, p3 are the start points, two control points and the end points respectively. With flatness defined as the maximum error from the orinal curve, and d the half width of the curve used for stroking (positive is to the right). func FlattenCubicBezier(p0, p1, p2, p3 math32.Vector2, d, tolerance float32) ppath.Path { tolerance = math32.Max(tolerance, ppath.Epsilon) // prevent infinite loop if user sets tolerance to zero p := ppath.Path{} start := p0.Add(CubicBezierNormal(p0, p1, p2, p3, 0.0, d)) p.MoveTo(start.X, start.Y) // 0 <= t1 <= 1 if t1 exists // 0 <= t1 <= t2 <= 1 if t1 and t2 both exist t1, t2 := findInflectionPointCubicBezier(p0, p1, p2, p3) if math32.IsNaN(t1) && math32.IsNaN(t2) { // There are no inflection points or cusps, approximate linearly by subdivision. FlattenSmoothCubicBezier(&p, p0, p1, p2, p3, d, tolerance) return p } // t1min <= t1max; with 0 <= t1max and t1min <= 1 // t2min <= t2max; with 0 <= t2max and t2min <= 1 t1min, t1max := findInflectionPointRangeCubicBezier(p0, p1, p2, p3, t1, tolerance) t2min, t2max := findInflectionPointRangeCubicBezier(p0, p1, p2, p3, t2, tolerance) if math32.IsNaN(t2) && t1min <= 0.0 && 1.0 <= t1max { // There is no second inflection point, and the first inflection point can be entirely approximated linearly. addCubicBezierLine(&p, p0, p1, p2, p3, 1.0, d) return p } if 0.0 < t1min { // Flatten up to t1min q0, q1, q2, q3, _, _, _, _ := cubicBezierSplit(p0, p1, p2, p3, t1min) FlattenSmoothCubicBezier(&p, q0, q1, q2, q3, d, tolerance) } if 0.0 < t1max && t1max < 1.0 && t1max < t2min { // t1 and t2 ranges do not overlap, approximate t1 linearly _, _, _, _, q0, q1, q2, q3 := cubicBezierSplit(p0, p1, p2, p3, t1max) addCubicBezierLine(&p, q0, q1, q2, q3, 0.0, d) if 1.0 <= t2min { // No t2 present, approximate the rest linearly by subdivision FlattenSmoothCubicBezier(&p, q0, q1, q2, q3, d, tolerance) return p } } else if 1.0 <= t2min { // No t2 present and t1max is past the end of the curve, approximate linearly addCubicBezierLine(&p, p0, p1, p2, p3, 1.0, d) return p } // t1 and t2 exist and ranges might overlap if 0.0 < t2min { if t2min < t1max { // t2 range starts inside t1 range, approximate t1 range linearly _, _, _, _, q0, q1, q2, q3 := cubicBezierSplit(p0, p1, p2, p3, t1max) addCubicBezierLine(&p, q0, q1, q2, q3, 0.0, d) } else { // no overlap _, _, _, _, q0, q1, q2, q3 := cubicBezierSplit(p0, p1, p2, p3, t1max) t2minq := (t2min - t1max) / (1 - t1max) q0, q1, q2, q3, _, _, _, _ = cubicBezierSplit(q0, q1, q2, q3, t2minq) FlattenSmoothCubicBezier(&p, q0, q1, q2, q3, d, tolerance) } } // handle (the rest of) t2 if t2max < 1.0 { _, _, _, _, q0, q1, q2, q3 := cubicBezierSplit(p0, p1, p2, p3, t2max) addCubicBezierLine(&p, q0, q1, q2, q3, 0.0, d) FlattenSmoothCubicBezier(&p, q0, q1, q2, q3, d, tolerance) } else { // t2max extends beyond 1 addCubicBezierLine(&p, p0, p1, p2, p3, 1.0, d) } return p } // split the curve and replace it by lines as long as (maximum deviation <= tolerance) is maintained func FlattenSmoothCubicBezier(p *ppath.Path, p0, p1, p2, p3 math32.Vector2, d, tolerance float32) { t := float32(0.0) for t < 1.0 { D := p1.Sub(p0) if ppath.EqualPoint(p0, p1) { // p0 == p1, base on p2 D = p2.Sub(p0) if ppath.EqualPoint(p0, p2) { // p0 == p1 == p2, curve is a straight line from p0 to p3 p.LineTo(p3.X, p3.Y) return } } denom := D.Length() // equal to r1 // effective flatness distorts the stroke width as both sides have different cuts //effFlatness := flatness / (1.0 - d*s2nom/(denom*denom*denom)*2.0/3.0) s2nom := D.Cross(p2.Sub(p0)) s2inv := denom / s2nom t2 := 2.0 * math32.Sqrt(tolerance*math32.Abs(s2inv)/3.0) // if s2 is small, s3 may represent the curvature more accurately // we cannot calculate the effective flatness here s3nom := D.Cross(p3.Sub(p0)) s3inv := denom / s3nom t3 := 2.0 * math32.Cbrt(tolerance*math32.Abs(s3inv)) // choose whichever is most curved, P2-P0 or P3-P0 t = math32.Min(t2, t3) if 1.0 <= t { break } _, _, _, _, p0, p1, p2, p3 = cubicBezierSplit(p0, p1, p2, p3, t) addCubicBezierLine(p, p0, p1, p2, p3, 0.0, d) } addCubicBezierLine(p, p0, p1, p2, p3, 1.0, d) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package intersect import ( "slices" "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" ) // curvature returns the curvature of the path at the given index // into ppath.Path and t in [0.0,1.0]. ppath.Path must not contain subpaths, // and will return the path's starting curvature when i points // to a MoveTo, or the path's final curvature when i points to // a Close of zero-length. func curvature(p ppath.Path, i int, t float32) float32 { last := len(p) if p[last-1] == ppath.Close && ppath.EqualPoint(math32.Vec2(p[last-ppath.CmdLen(ppath.Close)-3], p[last-ppath.CmdLen(ppath.Close)-2]), math32.Vec2(p[last-3], p[last-2])) { // point-closed last -= ppath.CmdLen(ppath.Close) } if i == 0 { // get path's starting direction when i points to MoveTo i = 4 t = 0.0 } else if i < len(p) && i == last { // get path's final direction when i points to zero-length Close i -= ppath.CmdLen(p[i-1]) t = 1.0 } if i < 0 || len(p) <= i || last < i+ppath.CmdLen(p[i]) { return 0.0 } cmd := p[i] var start math32.Vector2 if i == 0 { start = math32.Vec2(p[last-3], p[last-2]) } else { start = math32.Vec2(p[i-3], p[i-2]) } i += ppath.CmdLen(cmd) end := math32.Vec2(p[i-3], p[i-2]) switch cmd { case ppath.LineTo, ppath.Close: return 0.0 case ppath.QuadTo: cp := math32.Vec2(p[i-5], p[i-4]) return 1.0 / quadraticBezierCurvatureRadius(start, cp, end, t) case ppath.CubeTo: cp1 := math32.Vec2(p[i-7], p[i-6]) cp2 := math32.Vec2(p[i-5], p[i-4]) return 1.0 / CubicBezierCurvatureRadius(start, cp1, cp2, end, t) case ppath.ArcTo: rx, ry, phi := p[i-7], p[i-6], p[i-5] large, sweep := ppath.ToArcFlags(p[i-4]) _, _, theta0, theta1 := ppath.EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) theta := theta0 + t*(theta1-theta0) return 1.0 / EllipseCurvatureRadius(rx, ry, sweep, theta) } return 0.0 } // Curvature returns the curvature of the path at the given segment // and t in [0.0,1.0] along that path. It is zero for straight lines // and for non-existing segments. func Curvature(p ppath.Path, seg int, t float32) float32 { if len(p) <= 4 { return 0.0 } curSeg := 0 iStart, iSeg, iEnd := 0, 0, 0 for i := 0; i < len(p); { cmd := p[i] if cmd == ppath.MoveTo { if seg < curSeg { pi := p[iStart:iEnd] return curvature(pi, iSeg-iStart, t) } iStart = i } if seg == curSeg { iSeg = i } i += ppath.CmdLen(cmd) } return 0.0 // if segment doesn't exist } // windings counts intersections of ray with path. // ppath.Paths that cross downwards are negative and upwards are positive. // It returns the windings excluding the start position and the // windings of the start position itself. If the windings of the // start position is not zero, the start position is on a boundary. func windings(zs []Intersection) (int, bool) { // There are four particular situations to be aware of. Whenever the path is horizontal it // will be parallel to the ray, and usually overlapping. Either we have: // - a starting point to the left of the overlapping section: ignore the overlapping // intersections so that it appears as a regular intersection, albeit at the endpoints // of two segments, which may either cancel out to zero (top or bottom edge) or add up to // 1 or -1 if the path goes upwards or downwards respectively before/after the overlap. // - a starting point on the left-hand corner of an overlapping section: ignore if either // intersection of an endpoint pair (t=0,t=1) is overlapping, but count for nb upon // leaving the overlap. // - a starting point in the middle of an overlapping section: same as above // - a starting point on the right-hand corner of an overlapping section: intersections are // tangent and thus already ignored for n, but for nb we should ignore the intersection with // a 0/180 degree direction, and count the other n := 0 boundary := false for i := 0; i < len(zs); i++ { z := zs[i] if z.T[0] == 0.0 { boundary = true continue } d := 1 if z.Into() { d = -1 // downwards } if z.T[1] != 0.0 && z.T[1] != 1.0 { if !z.Same { n += d } } else if i+1 < len(zs) { same := z.Same || (len(zs) > i+1 && zs[i+1].Same) if !same && len(zs) > i+1 { if z.Into() == zs[i+1].Into() { n += d } } i++ } } return n, boundary } // Windings returns the number of windings at the given point, // i.e. the sum of windings for each time a ray from (x,y) // towards (∞,y) intersects the path. Counter clock-wise // intersections count as positive, while clock-wise intersections // count as negative. Additionally, it returns whether the point // is on a path's boundary (which counts as being on the exterior). func Windings(p ppath.Path, x, y float32) (int, bool) { n := 0 boundary := false for _, pi := range p.Split() { zs := RayIntersections(pi, x, y) if ni, boundaryi := windings(zs); boundaryi { boundary = true } else { n += ni } } return n, boundary } // Crossings returns the number of crossings with the path from the // given point outwards, i.e. the number of times a ray from (x,y) // towards (∞,y) intersects the path. Additionally, it returns whether // the point is on a path's boundary (which does not count towards // the number of crossings). func Crossings(p ppath.Path, x, y float32) (int, bool) { n := 0 boundary := false for _, pi := range p.Split() { // Count intersections of ray with path. Count half an intersection on boundaries. ni := 0.0 for _, z := range RayIntersections(pi, x, y) { if z.T[0] == 0.0 { boundary = true } else if !z.Same { if z.T[1] == 0.0 || z.T[1] == 1.0 { ni += 0.5 } else { ni += 1.0 } } else if z.T[1] == 0.0 || z.T[1] == 1.0 { ni -= 0.5 } } n += int(ni) } return n, boundary } // Contains returns whether the point (x,y) is contained/filled by the path. // This depends on the ppath.FillRules. It uses a ray from (x,y) toward (∞,y) and // counts the number of intersections with the path. // When the point is on the boundary it is considered to be on the path's exterior. func Contains(p ppath.Path, x, y float32, fillRule ppath.FillRules) bool { n, boundary := Windings(p, x, y) if boundary { return true } return fillRule.Fills(n) } // CCW returns true when the path is counter clockwise oriented at its // bottom-right-most coordinate. It is most useful when knowing that // the path does not self-intersect as it will tell you if the entire // path is CCW or not. It will only return the result for the first subpath. // It will return true for an empty path or a straight line. // It may not return a valid value when the right-most point happens to be a // (self-)overlapping segment. func CCW(p ppath.Path) bool { if len(p) <= 4 || (p[4] == ppath.LineTo || p[4] == ppath.Close) && len(p) <= 4+ppath.CmdLen(p[4]) { // empty path or single straight segment return true } p = XMonotone(p) // pick bottom-right-most coordinate of subpath, as we know its left-hand side is filling k, kMax := 4, len(p) if p[kMax-1] == ppath.Close { kMax -= ppath.CmdLen(ppath.Close) } for i := 4; i < len(p); { cmd := p[i] if cmd == ppath.MoveTo { // only handle first subpath kMax = i break } i += ppath.CmdLen(cmd) if x, y := p[i-3], p[i-2]; p[k-3] < x || ppath.Equal(p[k-3], x) && y < p[k-2] { k = i } } // get coordinates of previous and next segments var kPrev int if k == 4 { kPrev = kMax } else { kPrev = k - ppath.CmdLen(p[k-1]) } var angleNext float32 anglePrev := ppath.AngleNorm(ppath.Angle(ppath.DirectionIndex(p, kPrev, 1.0)) + math32.Pi) if k == kMax { // use implicit close command angleNext = ppath.Angle(math32.Vec2(p[1], p[2]).Sub(math32.Vec2(p[k-3], p[k-2]))) } else { angleNext = ppath.Angle(ppath.DirectionIndex(p, k, 0.0)) } if ppath.Equal(anglePrev, angleNext) { // segments have the same direction at their right-most point // one or both are not straight lines, check if curvature is different var curvNext float32 curvPrev := -curvature(p, kPrev, 1.0) if k == kMax { // use implicit close command curvNext = 0.0 } else { curvNext = curvature(p, k, 0.0) } if !ppath.Equal(curvPrev, curvNext) { // ccw if curvNext is smaller than curvPrev return curvNext < curvPrev } } return (angleNext - anglePrev) < 0.0 } // Filling returns whether each subpath gets filled or not. // Whether a path is filled depends on the ppath.FillRules and whether it // negates another path. If a subpath is not closed, it is implicitly // assumed to be closed. func Filling(p ppath.Path, fillRule ppath.FillRules) []bool { ps := p.Split() filling := make([]bool, len(ps)) for i, pi := range ps { // get current subpath's winding n := 0 if CCW(pi) { n++ } else { n-- } // sum windings from other subpaths pos := math32.Vec2(pi[1], pi[2]) for j, pj := range ps { if i == j { continue } zs := RayIntersections(pj, pos.X, pos.Y) if ni, boundaryi := windings(zs); !boundaryi { n += ni } else { // on the boundary, check if around the interior or exterior of pos } } filling[i] = fillRule.Fills(n) } return filling } // Length returns the length of the path in millimeters. // The length is approximated for cubic Béziers. func Length(p ppath.Path) float32 { d := float32(0.0) var start, end math32.Vector2 for i := 0; i < len(p); { cmd := p[i] switch cmd { case ppath.MoveTo: end = math32.Vec2(p[i+1], p[i+2]) case ppath.LineTo, ppath.Close: end = math32.Vec2(p[i+1], p[i+2]) d += end.Sub(start).Length() case ppath.QuadTo: cp := math32.Vec2(p[i+1], p[i+2]) end = math32.Vec2(p[i+3], p[i+4]) d += quadraticBezierLength(start, cp, end) case ppath.CubeTo: cp1 := math32.Vec2(p[i+1], p[i+2]) cp2 := math32.Vec2(p[i+3], p[i+4]) end = math32.Vec2(p[i+5], p[i+6]) d += cubicBezierLength(start, cp1, cp2, end) case ppath.ArcTo: var rx, ry, phi float32 var large, sweep bool rx, ry, phi, large, sweep, end = p.ArcToPoints(i) _, _, theta1, theta2 := ppath.EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) d += ellipseLength(rx, ry, theta1, theta2) } i += ppath.CmdLen(cmd) start = end } return d } // IsFlat returns true if the path consists of solely line segments, // that is only MoveTo, ppath.LineTo and Close commands. func IsFlat(p ppath.Path) bool { for i := 0; i < len(p); { cmd := p[i] if cmd != ppath.MoveTo && cmd != ppath.LineTo && cmd != ppath.Close { return false } i += ppath.CmdLen(cmd) } return true } // SplitAt splits the path into separate paths at the specified // intervals (given in millimeters) along the path. func SplitAt(p ppath.Path, ts ...float32) []ppath.Path { if len(ts) == 0 { return []ppath.Path{p} } slices.Sort(ts) if ts[0] == 0.0 { ts = ts[1:] } j := 0 // index into ts T := float32(0.0) // current position along curve qs := []ppath.Path{} q := ppath.Path{} push := func() { qs = append(qs, q) q = ppath.Path{} } if 0 < len(p) && p[0] == ppath.MoveTo { q.MoveTo(p[1], p[2]) } for _, ps := range p.Split() { var start, end math32.Vector2 for i := 0; i < len(ps); { cmd := ps[i] switch cmd { case ppath.MoveTo: end = math32.Vec2(p[i+1], p[i+2]) case ppath.LineTo, ppath.Close: end = math32.Vec2(p[i+1], p[i+2]) if j == len(ts) { q.LineTo(end.X, end.Y) } else { dT := end.Sub(start).Length() Tcurve := T for j < len(ts) && T < ts[j] && ts[j] <= T+dT { tpos := (ts[j] - T) / dT pos := start.Lerp(end, tpos) Tcurve = ts[j] q.LineTo(pos.X, pos.Y) push() q.MoveTo(pos.X, pos.Y) j++ } if Tcurve < T+dT { q.LineTo(end.X, end.Y) } T += dT } case ppath.QuadTo: cp := math32.Vec2(p[i+1], p[i+2]) end = math32.Vec2(p[i+3], p[i+4]) if j == len(ts) { q.QuadTo(cp.X, cp.Y, end.X, end.Y) } else { speed := func(t float32) float32 { return ppath.QuadraticBezierDeriv(start, cp, end, t).Length() } invL, dT := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7, speed, 0.0, 1.0) t0 := float32(0.0) r0, r1, r2 := start, cp, end for j < len(ts) && T < ts[j] && ts[j] <= T+dT { t := invL(ts[j] - T) tsub := (t - t0) / (1.0 - t0) t0 = t var q1 math32.Vector2 _, q1, _, r0, r1, r2 = quadraticBezierSplit(r0, r1, r2, tsub) q.QuadTo(q1.X, q1.Y, r0.X, r0.Y) push() q.MoveTo(r0.X, r0.Y) j++ } if !ppath.Equal(t0, 1.0) { q.QuadTo(r1.X, r1.Y, r2.X, r2.Y) } T += dT } case ppath.CubeTo: cp1 := math32.Vec2(p[i+1], p[i+2]) cp2 := math32.Vec2(p[i+3], p[i+4]) end = math32.Vec2(p[i+5], p[i+6]) if j == len(ts) { q.CubeTo(cp1.X, cp1.Y, cp2.X, cp2.Y, end.X, end.Y) } else { speed := func(t float32) float32 { // splitting on inflection points does not improve output return ppath.CubicBezierDeriv(start, cp1, cp2, end, t).Length() } N := 20 + 20*cubicBezierNumInflections(start, cp1, cp2, end) // TODO: needs better N invL, dT := invSpeedPolynomialChebyshevApprox(N, gaussLegendre7, speed, 0.0, 1.0) t0 := float32(0.0) r0, r1, r2, r3 := start, cp1, cp2, end for j < len(ts) && T < ts[j] && ts[j] <= T+dT { t := invL(ts[j] - T) tsub := (t - t0) / (1.0 - t0) t0 = t var q1, q2 math32.Vector2 _, q1, q2, _, r0, r1, r2, r3 = cubicBezierSplit(r0, r1, r2, r3, tsub) q.CubeTo(q1.X, q1.Y, q2.X, q2.Y, r0.X, r0.Y) push() q.MoveTo(r0.X, r0.Y) j++ } if !ppath.Equal(t0, 1.0) { q.CubeTo(r1.X, r1.Y, r2.X, r2.Y, r3.X, r3.Y) } T += dT } case ppath.ArcTo: var rx, ry, phi float32 var large, sweep bool rx, ry, phi, large, sweep, end = p.ArcToPoints(i) cx, cy, theta1, theta2 := ppath.EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) if j == len(ts) { q.ArcTo(rx, ry, phi, large, sweep, end.X, end.Y) } else { speed := func(theta float32) float32 { return ppath.EllipseDeriv(rx, ry, 0.0, true, theta).Length() } invL, dT := invSpeedPolynomialChebyshevApprox(10, gaussLegendre7, speed, theta1, theta2) startTheta := theta1 nextLarge := large for j < len(ts) && T < ts[j] && ts[j] <= T+dT { theta := invL(ts[j] - T) mid, large1, large2, ok := ellipseSplit(rx, ry, phi, cx, cy, startTheta, theta2, theta) if !ok { panic("theta not in elliptic arc range for splitting") } q.ArcTo(rx, ry, phi, large1, sweep, mid.X, mid.Y) push() q.MoveTo(mid.X, mid.Y) startTheta = theta nextLarge = large2 j++ } if !ppath.Equal(startTheta, theta2) { q.ArcTo(rx, ry, phi*180.0/math32.Pi, nextLarge, sweep, end.X, end.Y) } T += dT } } i += ppath.CmdLen(cmd) start = end } } if ppath.CmdLen(ppath.MoveTo) < len(q) { push() } return qs } // XMonotone replaces all Bézier and arc segments to be x-monotone // and returns a new path, that is each path segment is either increasing // or decreasing with X while moving across the segment. // This is always true for line segments. func XMonotone(p ppath.Path) ppath.Path { quad := func(p0, p1, p2 math32.Vector2) ppath.Path { return xmonotoneQuadraticBezier(p0, p1, p2) } cube := func(p0, p1, p2, p3 math32.Vector2) ppath.Path { return xmonotoneCubicBezier(p0, p1, p2, p3) } arc := func(start math32.Vector2, rx, ry, phi float32, large, sweep bool, end math32.Vector2) ppath.Path { return xmonotoneEllipticArc(start, rx, ry, phi, large, sweep, end) } return p.Replace(nil, quad, cube, arc) } // Bounds returns the exact bounding box rectangle of the path. func Bounds(p ppath.Path) math32.Box2 { if len(p) < 4 { return math32.Box2{} } // first command is MoveTo start, end := math32.Vec2(p[1], p[2]), math32.Vector2{} xmin, xmax := start.X, start.X ymin, ymax := start.Y, start.Y for i := 4; i < len(p); { cmd := p[i] switch cmd { case ppath.MoveTo, ppath.LineTo, ppath.Close: end = math32.Vec2(p[i+1], p[i+2]) xmin = math32.Min(xmin, end.X) xmax = math32.Max(xmax, end.X) ymin = math32.Min(ymin, end.Y) ymax = math32.Max(ymax, end.Y) case ppath.QuadTo: cp := math32.Vec2(p[i+1], p[i+2]) end = math32.Vec2(p[i+3], p[i+4]) xmin = math32.Min(xmin, end.X) xmax = math32.Max(xmax, end.X) if tdenom := (start.X - 2*cp.X + end.X); !ppath.Equal(tdenom, 0.0) { if t := (start.X - cp.X) / tdenom; inIntervalExclusive(t, 0.0, 1.0) { x := quadraticBezierPos(start, cp, end, t) xmin = math32.Min(xmin, x.X) xmax = math32.Max(xmax, x.X) } } ymin = math32.Min(ymin, end.Y) ymax = math32.Max(ymax, end.Y) if tdenom := (start.Y - 2*cp.Y + end.Y); !ppath.Equal(tdenom, 0.0) { if t := (start.Y - cp.Y) / tdenom; inIntervalExclusive(t, 0.0, 1.0) { y := quadraticBezierPos(start, cp, end, t) ymin = math32.Min(ymin, y.Y) ymax = math32.Max(ymax, y.Y) } } case ppath.CubeTo: cp1 := math32.Vec2(p[i+1], p[i+2]) cp2 := math32.Vec2(p[i+3], p[i+4]) end = math32.Vec2(p[i+5], p[i+6]) a := -start.X + 3*cp1.X - 3*cp2.X + end.X b := 2*start.X - 4*cp1.X + 2*cp2.X c := -start.X + cp1.X t1, t2 := solveQuadraticFormula(a, b, c) xmin = math32.Min(xmin, end.X) xmax = math32.Max(xmax, end.X) if !math32.IsNaN(t1) && inIntervalExclusive(t1, 0.0, 1.0) { x1 := cubicBezierPos(start, cp1, cp2, end, t1) xmin = math32.Min(xmin, x1.X) xmax = math32.Max(xmax, x1.X) } if !math32.IsNaN(t2) && inIntervalExclusive(t2, 0.0, 1.0) { x2 := cubicBezierPos(start, cp1, cp2, end, t2) xmin = math32.Min(xmin, x2.X) xmax = math32.Max(xmax, x2.X) } a = -start.Y + 3*cp1.Y - 3*cp2.Y + end.Y b = 2*start.Y - 4*cp1.Y + 2*cp2.Y c = -start.Y + cp1.Y t1, t2 = solveQuadraticFormula(a, b, c) ymin = math32.Min(ymin, end.Y) ymax = math32.Max(ymax, end.Y) if !math32.IsNaN(t1) && inIntervalExclusive(t1, 0.0, 1.0) { y1 := cubicBezierPos(start, cp1, cp2, end, t1) ymin = math32.Min(ymin, y1.Y) ymax = math32.Max(ymax, y1.Y) } if !math32.IsNaN(t2) && inIntervalExclusive(t2, 0.0, 1.0) { y2 := cubicBezierPos(start, cp1, cp2, end, t2) ymin = math32.Min(ymin, y2.Y) ymax = math32.Max(ymax, y2.Y) } case ppath.ArcTo: var rx, ry, phi float32 var large, sweep bool rx, ry, phi, large, sweep, end = p.ArcToPoints(i) cx, cy, theta0, theta1 := ppath.EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) // find the four extremes (top, bottom, left, right) and apply those who are between theta1 and theta2 // x(theta) = cx + rx*cos(theta)*cos(phi) - ry*sin(theta)*sin(phi) // y(theta) = cy + rx*cos(theta)*sin(phi) + ry*sin(theta)*cos(phi) // be aware that positive rotation appears clockwise in SVGs (non-Cartesian coordinate system) // we can now find the angles of the extremes sinphi, cosphi := math32.Sincos(phi) thetaRight := math32.Atan2(-ry*sinphi, rx*cosphi) thetaTop := math32.Atan2(rx*cosphi, ry*sinphi) thetaLeft := thetaRight + math32.Pi thetaBottom := thetaTop + math32.Pi dx := math32.Sqrt(rx*rx*cosphi*cosphi + ry*ry*sinphi*sinphi) dy := math32.Sqrt(rx*rx*sinphi*sinphi + ry*ry*cosphi*cosphi) if ppath.IsAngleBetween(thetaLeft, theta0, theta1) { xmin = math32.Min(xmin, cx-dx) } if ppath.IsAngleBetween(thetaRight, theta0, theta1) { xmax = math32.Max(xmax, cx+dx) } if ppath.IsAngleBetween(thetaBottom, theta0, theta1) { ymin = math32.Min(ymin, cy-dy) } if ppath.IsAngleBetween(thetaTop, theta0, theta1) { ymax = math32.Max(ymax, cy+dy) } xmin = math32.Min(xmin, end.X) xmax = math32.Max(xmax, end.X) ymin = math32.Min(ymin, end.Y) ymax = math32.Max(ymax, end.Y) } i += ppath.CmdLen(cmd) start = end } return math32.B2(xmin, ymin, xmax, ymax) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package intersect import ( "fmt" "io" "slices" "sort" "strings" "sync" "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" ) // BentleyOttmannEpsilon is the snap rounding grid used by the Bentley-Ottmann algorithm. // This prevents numerical issues. It must be larger than Epsilon since we use that to calculate // intersections between segments. It is the number of binary digits to keep. var BentleyOttmannEpsilon = float32(1e-8) // RayIntersections returns the intersections of a path with a ray starting at (x,y) to (∞,y). // An intersection is tangent only when it is at (x,y), i.e. the start of the ray. The parameter T // along the ray is zero at the start but NaN otherwise. Intersections are sorted along the ray. // This function runs in O(n) with n the number of path segments. func RayIntersections(p ppath.Path, x, y float32) []Intersection { var start, end, cp1, cp2 math32.Vector2 var zs []Intersection for i := 0; i < len(p); { cmd := p[i] switch cmd { case ppath.MoveTo: end = p.EndPoint(i) case ppath.LineTo, ppath.Close: end = p.EndPoint(i) ymin := math32.Min(start.Y, end.Y) ymax := math32.Max(start.Y, end.Y) xmax := math32.Max(start.X, end.X) if inInterval(y, ymin, ymax) && x <= xmax+ppath.Epsilon { zs = intersectionLineLine(zs, math32.Vector2{x, y}, math32.Vector2{xmax + 1.0, y}, start, end) } case ppath.QuadTo: cp1, end = p.QuadToPoints(i) ymin := math32.Min(math32.Min(start.Y, end.Y), cp1.Y) ymax := math32.Max(math32.Max(start.Y, end.Y), cp1.Y) xmax := math32.Max(math32.Max(start.X, end.X), cp1.X) if inInterval(y, ymin, ymax) && x <= xmax+ppath.Epsilon { zs = intersectionLineQuad(zs, math32.Vector2{x, y}, math32.Vector2{xmax + 1.0, y}, start, cp1, end) } case ppath.CubeTo: cp1, cp2, end = p.CubeToPoints(i) ymin := math32.Min(math32.Min(start.Y, end.Y), math32.Min(cp1.Y, cp2.Y)) ymax := math32.Max(math32.Max(start.Y, end.Y), math32.Max(cp1.Y, cp2.Y)) xmax := math32.Max(math32.Max(start.X, end.X), math32.Max(cp1.X, cp2.X)) if inInterval(y, ymin, ymax) && x <= xmax+ppath.Epsilon { zs = intersectionLineCube(zs, math32.Vector2{x, y}, math32.Vector2{xmax + 1.0, y}, start, cp1, cp2, end) } case ppath.ArcTo: var rx, ry, phi float32 var large, sweep bool rx, ry, phi, large, sweep, end = p.ArcToPoints(i) cx, cy, theta0, theta1 := ppath.EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) if inInterval(y, cy-math32.Max(rx, ry), cy+math32.Max(rx, ry)) && x <= cx+math32.Max(rx, ry)+ppath.Epsilon { zs = intersectionLineEllipse(zs, math32.Vector2{x, y}, math32.Vector2{cx + rx + 1.0, y}, math32.Vector2{cx, cy}, math32.Vector2{rx, ry}, phi, theta0, theta1) } } i += ppath.CmdLen(cmd) start = end } for i := range zs { if zs[i].T[0] != 0.0 { zs[i].T[0] = math32.NaN() } } sort.SliceStable(zs, func(i, j int) bool { if ppath.Equal(zs[i].X, zs[j].X) { return false } return zs[i].X < zs[j].X }) return zs } type pathOp int const ( opSettle pathOp = iota opAND opOR opNOT opXOR opDIV ) func (op pathOp) String() string { switch op { case opSettle: return "Settle" case opAND: return "AND" case opOR: return "OR" case opNOT: return "NOT" case opXOR: return "XOR" case opDIV: return "DIV" } return fmt.Sprintf("pathOp(%d)", op) } var boPointPool *sync.Pool var boNodePool *sync.Pool var boSquarePool *sync.Pool var boInitPoolsOnce = sync.OnceFunc(func() { boPointPool = &sync.Pool{New: func() any { return &SweepPoint{} }} boNodePool = &sync.Pool{New: func() any { return &SweepNode{} }} boSquarePool = &sync.Pool{New: func() any { return &toleranceSquare{} }} }) // Settle returns the "settled" path. It removes all self-intersections, orients all filling paths // CCW and all holes CW, and tries to split into subpaths if possible. Note that path p is // flattened unless q is already flat. ppath.Path q is implicitly closed. It runs in O((n + k) log n), // with n the sum of the number of segments, and k the number of intersections. func Settle(p ppath.Path, fillRule ppath.FillRules) ppath.Path { return bentleyOttmann(p.Split(), nil, opSettle, fillRule) } // SettlePaths is the same as [Settle], but faster if paths are already split. func SettlePaths(ps ppath.Paths, fillRule ppath.FillRules) ppath.Path { return bentleyOttmann(ps, nil, opSettle, fillRule) } // And returns the boolean path operation of path p AND q, i.e. the intersection of both. It // removes all self-intersections, orients all filling paths CCW and all holes CW, and tries to // split into subpaths if possible. Note that path p is flattened unless q is already flat. ppath.Path // q is implicitly closed. It runs in O((n + k) log n), with n the sum of the number of segments, // and k the number of intersections. func And(p ppath.Path, q ppath.Path) ppath.Path { return bentleyOttmann(p.Split(), q.Split(), opAND, ppath.NonZero) } // AndPaths is the same as [And], but faster if paths are already split. func AndPaths(ps ppath.Paths, qs ppath.Paths) ppath.Path { return bentleyOttmann(ps, qs, opAND, ppath.NonZero) } // Or returns the boolean path operation of path p OR q, i.e. the union of both. It // removes all self-intersections, orients all filling paths CCW and all holes CW, and tries to // split into subpaths if possible. Note that path p is flattened unless q is already flat. ppath.Path // q is implicitly closed. It runs in O((n + k) log n), with n the sum of the number of segments, // and k the number of intersections. func Or(p ppath.Path, q ppath.Path) ppath.Path { return bentleyOttmann(p.Split(), q.Split(), opOR, ppath.NonZero) } // OrPaths is the same as ppath.Path.Or, but faster if paths are already split. func OrPaths(ps ppath.Paths, qs ppath.Paths) ppath.Path { return bentleyOttmann(ps, qs, opOR, ppath.NonZero) } // Xor returns the boolean path operation of path p XOR q, i.e. the symmetric difference of both. // It removes all self-intersections, orients all filling paths CCW and all holes CW, and tries to // split into subpaths if possible. Note that path p is flattened unless q is already flat. ppath.Path // q is implicitly closed. It runs in O((n + k) log n), with n the sum of the number of segments, // and k the number of intersections. func Xor(p ppath.Path, q ppath.Path) ppath.Path { return bentleyOttmann(p.Split(), q.Split(), opXOR, ppath.NonZero) } // XorPaths is the same as [Xor], but faster if paths are already split. func XorPaths(ps ppath.Paths, qs ppath.Paths) ppath.Path { return bentleyOttmann(ps, qs, opXOR, ppath.NonZero) } // Not returns the boolean path operation of path p NOT q, i.e. the difference of both. // It removes all self-intersections, orients all filling paths CCW and all holes CW, and tries to // split into subpaths if possible. Note that path p is flattened unless q is already flat. ppath.Path // q is implicitly closed. It runs in O((n + k) log n), with n the sum of the number of segments, // and k the number of intersections. func Not(p ppath.Path, q ppath.Path) ppath.Path { return bentleyOttmann(p.Split(), q.Split(), opNOT, ppath.NonZero) } // NotPaths is the same as ppath.Path.Not, but faster if paths are already split. func NotPaths(ps ppath.Paths, qs ppath.Paths) ppath.Path { return bentleyOttmann(ps, qs, opNOT, ppath.NonZero) } // DivideBy returns the boolean path operation of path p DIV q, i.e. p divided by q. // It removes all self-intersections, orients all filling paths CCW and all holes CW, and tries to // split into subpaths if possible. Note that path p is flattened unless q is already flat. ppath.Path // q is implicitly closed. It runs in O((n + k) log n), with n the sum of the number of segments, // and k the number of intersections. func DivideBy(p ppath.Path, q ppath.Path) ppath.Path { return bentleyOttmann(p.Split(), q.Split(), opDIV, ppath.NonZero) } // DivideByPaths is the same as [DivideBy] but faster if paths are already split. func DivideByPaths(ps ppath.Paths, qs ppath.Paths) ppath.Path { return bentleyOttmann(ps, qs, opDIV, ppath.NonZero) } type SweepPoint struct { // initial data math32.Vector2 // position of this endpoint other *SweepPoint // pointer to the other endpoint of the segment segment int // segment index to distinguish self-overlapping segments // processing the queue node *SweepNode // used for fast accessing btree node in O(1) (instead of Find in O(log n)) // computing sweep fields windings int // windings of the same polygon (excluding this segment) otherWindings int // windings of the other polygon selfWindings int // positive if segment goes left-right (or bottom-top when vertical) otherSelfWindings int // used when merging overlapping segments prev *SweepPoint // segment below // building the polygon index int // index into result array resultWindings int // windings of the resulting polygon // bools at the end to optimize memory layout of struct clipping bool // is clipping path (otherwise is subject path) open bool // path is not closed (only for subject paths) left bool // point is left-end of segment vertical bool // segment is vertical increasing bool // original direction is left-right (or bottom-top) overlapped bool // segment's overlapping was handled inResult uint8 // in final result polygon (1 is once, 2 is twice for opDIV) } func (s *SweepPoint) InterpolateY(x float32) float32 { t := (x - s.X) / (s.other.X - s.X) return s.Lerp(s.other.Vector2, t).Y } // ToleranceEdgeY returns the y-value of the SweepPoint at the tolerance edges given by xLeft and // xRight, or at the endpoints of the SweepPoint, whichever comes first. func (s *SweepPoint) ToleranceEdgeY(xLeft, xRight float32) (float32, float32) { if !s.left { s = s.other } y0 := s.Y if s.X < xLeft { y0 = s.InterpolateY(xLeft) } y1 := s.other.Y if xRight <= s.other.X { y1 = s.InterpolateY(xRight) } return y0, y1 } func (s *SweepPoint) SplitAt(z math32.Vector2) (*SweepPoint, *SweepPoint) { // split segment at point r := boPointPool.Get().(*SweepPoint) l := boPointPool.Get().(*SweepPoint) *r, *l = *s.other, *s r.Vector2, l.Vector2 = z, z // update references r.other, s.other.other = s, l l.other, s.other = s.other, r l.node = nil return r, l } func (s *SweepPoint) Reverse() { s.left, s.other.left = !s.left, s.left s.increasing, s.other.increasing = !s.increasing, !s.other.increasing } func (s *SweepPoint) String() string { path := "P" if s.clipping { path = "Q" } arrow := "→" if !s.left { arrow = "←" } return fmt.Sprintf("%s-%v(%v%v%v)", path, s.segment, s.Vector2, arrow, s.other.Vector2) } // SweepEvents is a heap priority queue of sweep events. type SweepEvents []*SweepPoint func (q SweepEvents) Less(i, j int) bool { return q[i].LessH(q[j]) } func (q SweepEvents) Swap(i, j int) { q[i], q[j] = q[j], q[i] } func (q *SweepEvents) AddPathEndpoints(p ppath.Path, seg int, clipping bool) int { if len(p) == 0 { return seg } // TODO: change this if we allow non-flat paths // allocate all memory at once to prevent multiple allocations/memmoves below n := len(p) / 4 if cap(*q) < len(*q)+n { q2 := make(SweepEvents, len(*q), len(*q)+n) copy(q2, *q) *q = q2 } open := !p.Closed() start := math32.Vector2{p[1], p[2]} if math32.IsNaN(start.X) || math32.IsInf(start.X, 0.0) || math32.IsNaN(start.Y) || math32.IsInf(start.Y, 0.0) { panic("path has NaN or Inf") } for i := 4; i < len(p); { if p[i] != ppath.LineTo && p[i] != ppath.Close { panic("non-flat paths not supported") } n := ppath.CmdLen(p[i]) end := math32.Vector2{p[i+n-3], p[i+n-2]} if math32.IsNaN(end.X) || math32.IsInf(end.X, 0.0) || math32.IsNaN(end.Y) || math32.IsInf(end.Y, 0.0) { panic("path has NaN or Inf") } i += n seg++ if start == end { // skip zero-length lineTo or close command start = end continue } vertical := start.X == end.X increasing := start.X < end.X if vertical { increasing = start.Y < end.Y } a := boPointPool.Get().(*SweepPoint) b := boPointPool.Get().(*SweepPoint) *a = SweepPoint{ Vector2: start, clipping: clipping, open: open, segment: seg, left: increasing, increasing: increasing, vertical: vertical, } *b = SweepPoint{ Vector2: end, clipping: clipping, open: open, segment: seg, left: !increasing, increasing: increasing, vertical: vertical, } a.other = b b.other = a *q = append(*q, a, b) start = end } return seg } func (q SweepEvents) Init() { n := len(q) for i := n/2 - 1; 0 <= i; i-- { q.down(i, n) } } func (q *SweepEvents) Push(item *SweepPoint) { *q = append(*q, item) q.up(len(*q) - 1) } func (q *SweepEvents) Top() *SweepPoint { return (*q)[0] } func (q *SweepEvents) Pop() *SweepPoint { n := len(*q) - 1 q.Swap(0, n) q.down(0, n) items := (*q)[n] *q = (*q)[:n] return items } func (q *SweepEvents) Fix(i int) { if !q.down(i, len(*q)) { q.up(i) } } // from container/heap func (q SweepEvents) up(j int) { for { i := (j - 1) / 2 // parent if i == j || !q.Less(j, i) { break } q.Swap(i, j) j = i } } func (q SweepEvents) down(i0, n int) bool { i := i0 for { j1 := 2*i + 1 if n <= j1 || j1 < 0 { // j1 < 0 after int overflow break } j := j1 // left child if j2 := j1 + 1; j2 < n && q.Less(j2, j1) { j = j2 // = 2*i + 2 // right child } if !q.Less(j, i) { break } q.Swap(i, j) i = j } return i0 < i } func (q SweepEvents) Print(w io.Writer) { q2 := make(SweepEvents, len(q)) copy(q2, q) q = q2 n := len(q) - 1 for 0 < n { q.Swap(0, n) q.down(0, n) n-- } width := int(math32.Max(0.0, math32.Log10(float32(len(q)-1)))) + 1 for k := len(q) - 1; 0 <= k; k-- { fmt.Fprintf(w, "%*d %v\n", width, len(q)-1-k, q[k]) } return } func (q SweepEvents) String() string { sb := strings.Builder{} q.Print(&sb) str := sb.String() if 0 < len(str) { str = str[:len(str)-1] } return str } type SweepNode struct { parent, left, right *SweepNode height int *SweepPoint } func (n *SweepNode) Prev() *SweepNode { // go left if n.left != nil { n = n.left for n.right != nil { n = n.right // find the right-most of current subtree } return n } for n.parent != nil && n.parent.left == n { n = n.parent // find first parent for which we're right } return n.parent // can be nil } func (n *SweepNode) Next() *SweepNode { // go right if n.right != nil { n = n.right for n.left != nil { n = n.left // find the left-most of current subtree } return n } for n.parent != nil && n.parent.right == n { n = n.parent // find first parent for which we're left } return n.parent // can be nil } func (a *SweepNode) swap(b *SweepNode) { a.SweepPoint, b.SweepPoint = b.SweepPoint, a.SweepPoint a.SweepPoint.node, b.SweepPoint.node = a, b } //func (n *SweepNode) fix() (*SweepNode, int) { // move := 0 // if prev := n.Prev(); prev != nil && 0 < prev.CompareV(n.SweepPoint, false) { // // move down // n.swap(prev) // n, prev = prev, n // move-- // // for prev = prev.Prev(); prev != nil; prev = prev.Prev() { // if prev.CompareV(n.SweepPoint, false) < 0 { // break // } // n.swap(prev) // n, prev = prev, n // move-- // } // } else if next := n.Next(); next != nil && next.CompareV(n.SweepPoint, false) < 0 { // // move up // n.swap(next) // n, next = next, n // move++ // // for next = next.Next(); next != nil; next = next.Next() { // if 0 < next.CompareV(n.SweepPoint, false) { // break // } // n.swap(next) // n, next = next, n // move++ // } // } // return n, move //} func (n *SweepNode) balance() int { r := 0 if n.left != nil { r -= n.left.height } if n.right != nil { r += n.right.height } return r } func (n *SweepNode) updateHeight() { n.height = 0 if n.left != nil { n.height = n.left.height } if n.right != nil && n.height < n.right.height { n.height = n.right.height } n.height++ } func (n *SweepNode) swapChild(a, b *SweepNode) { if n.right == a { n.right = b } else { n.left = b } if b != nil { b.parent = n } } func (a *SweepNode) rotateLeft() *SweepNode { b := a.right if a.parent != nil { a.parent.swapChild(a, b) } else { b.parent = nil } a.parent = b if a.right = b.left; a.right != nil { a.right.parent = a } b.left = a return b } func (a *SweepNode) rotateRight() *SweepNode { b := a.left if a.parent != nil { a.parent.swapChild(a, b) } else { b.parent = nil } a.parent = b if a.left = b.right; a.left != nil { a.left.parent = a } b.right = a return b } func (n *SweepNode) print(w io.Writer, prefix string, cmp int) { c := "" if cmp < 0 { c = "│ " } else if 0 < cmp { c = " " } if n.right != nil { n.right.print(w, prefix+c, 1) } else if n.left != nil { fmt.Fprintf(w, "%v%v┌─nil\n", prefix, c) } c = "" if 0 < cmp { c = "┌─" } else if cmp < 0 { c = "└─" } fmt.Fprintf(w, "%v%v%v\n", prefix, c, n.SweepPoint) c = "" if 0 < cmp { c = "│ " } else if cmp < 0 { c = " " } if n.left != nil { n.left.print(w, prefix+c, -1) } else if n.right != nil { fmt.Fprintf(w, "%v%v└─nil\n", prefix, c) } } func (n *SweepNode) Print(w io.Writer) { n.print(w, "", 0) } // TODO: test performance versus (2,4)-tree (current LEDA implementation), (2,16)-tree (as proposed by S. Naber/Näher in "Comparison of search-tree data structures in LEDA. Personal communication" apparently), RB-tree (likely a good candidate), and an AA-tree (simpler implementation may be faster). Perhaps an unbalanced (e.g. Treap) works well due to the high number of insertions/deletions. type SweepStatus struct { root *SweepNode } func (s *SweepStatus) newNode(item *SweepPoint) *SweepNode { n := boNodePool.Get().(*SweepNode) n.parent = nil n.left = nil n.right = nil n.height = 1 n.SweepPoint = item n.SweepPoint.node = n return n } func (s *SweepStatus) returnNode(n *SweepNode) { n.SweepPoint.node = nil n.SweepPoint = nil // help the GC boNodePool.Put(n) } func (s *SweepStatus) find(item *SweepPoint) (*SweepNode, int) { n := s.root for n != nil { cmp := item.CompareV(n.SweepPoint) if cmp < 0 { if n.left == nil { return n, -1 } n = n.left } else if 0 < cmp { if n.right == nil { return n, 1 } n = n.right } else { break } } return n, 0 } func (s *SweepStatus) rebalance(n *SweepNode) { for { oheight := n.height if balance := n.balance(); balance == 2 { // Tree is excessively right-heavy, rotate it to the left. if n.right != nil && n.right.balance() < 0 { // Right tree is left-heavy, which would cause the next rotation to result in // overall left-heaviness. Rotate the right tree to the right to counteract this. n.right = n.right.rotateRight() n.right.right.updateHeight() } n = n.rotateLeft() n.left.updateHeight() } else if balance == -2 { // Tree is excessively left-heavy, rotate it to the right if n.left != nil && 0 < n.left.balance() { // The left tree is right-heavy, which would cause the next rotation to result in // overall right-heaviness. Rotate the left tree to the left to compensate. n.left = n.left.rotateLeft() n.left.left.updateHeight() } n = n.rotateRight() n.right.updateHeight() } else if balance < -2 || 2 < balance { panic("Tree too far out of shape!") } n.updateHeight() if n.parent == nil { s.root = n return } if oheight == n.height { return } n = n.parent } } func (s *SweepStatus) String() string { if s.root == nil { return "nil" } sb := strings.Builder{} s.root.Print(&sb) str := sb.String() if 0 < len(str) { str = str[:len(str)-1] } return str } func (s *SweepStatus) First() *SweepNode { if s.root == nil { return nil } n := s.root for n.left != nil { n = n.left } return n } func (s *SweepStatus) Last() *SweepNode { if s.root == nil { return nil } n := s.root for n.right != nil { n = n.right } return n } // Find returns the node equal to item. May return nil. func (s *SweepStatus) Find(item *SweepPoint) *SweepNode { n, cmp := s.find(item) if cmp == 0 { return n } return nil } func (s *SweepStatus) FindPrevNext(item *SweepPoint) (*SweepNode, *SweepNode) { if s.root == nil { return nil, nil } n, cmp := s.find(item) if cmp < 0 { return n.Prev(), n } else if 0 < cmp { return n, n.Next() } else { return n.Prev(), n.Next() } } func (s *SweepStatus) Insert(item *SweepPoint) *SweepNode { if s.root == nil { s.root = s.newNode(item) return s.root } rebalance := false n, cmp := s.find(item) if cmp < 0 { // lower n.left = s.newNode(item) n.left.parent = n rebalance = n.right == nil } else if 0 < cmp { // higher n.right = s.newNode(item) n.right.parent = n rebalance = n.left == nil } else { // equal, replace n.SweepPoint.node = nil n.SweepPoint = item n.SweepPoint.node = n return n } if rebalance && n.parent != nil { n.height++ s.rebalance(n.parent) } if cmp < 0 { return n.left } else { return n.right } } func (s *SweepStatus) InsertAfter(n *SweepNode, item *SweepPoint) *SweepNode { var cur *SweepNode rebalance := false if n == nil { if s.root == nil { s.root = s.newNode(item) return s.root } // insert as left-most node in tree n = s.root for n.left != nil { n = n.left } n.left = s.newNode(item) n.left.parent = n rebalance = n.right == nil cur = n.left } else if n.right == nil { // insert directly to the right of n n.right = s.newNode(item) n.right.parent = n rebalance = n.left == nil cur = n.right } else { // insert next to n at a deeper level n = n.right for n.left != nil { n = n.left } n.left = s.newNode(item) n.left.parent = n rebalance = n.right == nil cur = n.left } if rebalance && n.parent != nil { n.height++ s.rebalance(n.parent) } return cur } func (s *SweepStatus) Remove(n *SweepNode) { ancestor := n.parent if n.left == nil || n.right == nil { // no children or one child child := n.left if n.left == nil { child = n.right } if n.parent != nil { n.parent.swapChild(n, child) } else { s.root = child } if child != nil { child.parent = n.parent } } else { // two children succ := n.right for succ.left != nil { succ = succ.left } ancestor = succ.parent // rebalance from here if succ.parent == n { // succ is child of n ancestor = succ } succ.parent.swapChild(succ, succ.right) // swap succesor with deleted node succ.parent, succ.left, succ.right = n.parent, n.left, n.right if n.parent != nil { n.parent.swapChild(n, succ) } else { s.root = succ } if n.left != nil { n.left.parent = succ } if n.right != nil { n.right.parent = succ } } // rebalance all ancestors for ; ancestor != nil; ancestor = ancestor.parent { s.rebalance(ancestor) } s.returnNode(n) return } func (s *SweepStatus) Clear() { n := s.First() for n != nil { cur := n n = n.Next() s.returnNode(cur) } s.root = nil } func (a *SweepPoint) LessH(b *SweepPoint) bool { // used for sweep queue if a.X != b.X { return a.X < b.X // sort left to right } else if a.Y != b.Y { return a.Y < b.Y // then bottom to top } else if a.left != b.left { return b.left // handle right-endpoints before left-endpoints } else if a.compareTangentsV(b) < 0 { return true // sort upwards, this ensures CCW orientation order of result } return false } func (a *SweepPoint) CompareH(b *SweepPoint) int { // used for sweep queue // sort left-to-right, then bottom-to-top, then right-endpoints before left-endpoints, and then // sort upwards to ensure a CCW orientation of the result if a.X < b.X { return -1 } else if b.X < a.X { return 1 } else if a.Y < b.Y { return -1 } else if b.Y < a.Y { return 1 } else if !a.left && b.left { return -1 } else if a.left && !b.left { return 1 } return a.compareTangentsV(b) } func (a *SweepPoint) compareOverlapsV(b *SweepPoint) int { // compare segments vertically that overlap (ie. are the same) if a.clipping != b.clipping { // for equal segments, clipping path is virtually on top (or left if vertical) of subject if b.clipping { return -1 } else { return 1 } } // equal segment on same path, sort by segment index if a.segment != b.segment { if a.segment < b.segment { return -1 } else { return 1 } } return 0 } func (a *SweepPoint) compareTangentsV(b *SweepPoint) int { // compare segments vertically at a.X, b.X <= a.X, and a and b coincide at (a.X,a.Y) // note that a.left==b.left, we may be comparing right-endpoints sign := 1 if !a.left { sign = -1 } if a.vertical { // a is vertical if b.vertical { // a and b are vertical if a.Y == b.Y { return sign * a.compareOverlapsV(b) } else if a.Y < b.Y { return -1 } else { return 1 } } return 1 } else if b.vertical { // b is vertical return -1 } if a.other.X == b.other.X && a.other.Y == b.other.Y { return sign * a.compareOverlapsV(b) } else if a.left && a.other.X < b.other.X || !a.left && b.other.X < a.other.X { by := b.InterpolateY(a.other.X) // b's y at a's other if a.other.Y == by { return sign * a.compareOverlapsV(b) } else if a.other.Y < by { return sign * -1 } else { return sign * 1 } } else { ay := a.InterpolateY(b.other.X) // a's y at b's other if ay == b.other.Y { return sign * a.compareOverlapsV(b) } else if ay < b.other.Y { return sign * -1 } else { return sign * 1 } } } func (a *SweepPoint) compareV(b *SweepPoint) int { // compare segments vertically at a.X and b.X < a.X // note that by may be infinite/large for fully/nearly vertical segments by := b.InterpolateY(a.X) // b's y at a's left if a.Y == by { return a.compareTangentsV(b) } else if a.Y < by { return -1 } else { return 1 } } func (a *SweepPoint) CompareV(b *SweepPoint) int { // used for sweep status, a is the point to be inserted / found if a.X == b.X { // left-point at same X if a.Y == b.Y { // left-point the same return a.compareTangentsV(b) } else if a.Y < b.Y { return -1 } else { return 1 } } else if a.X < b.X { // a starts to the left of b return -b.compareV(a) } else { // a starts to the right of b return a.compareV(b) } } //type SweepPointPair [2]*SweepPoint // //func (pair SweepPointPair) Swapped() SweepPointPair { // return SweepPointPair{pair[1], pair[0]} //} func addIntersections(zs []math32.Vector2, queue *SweepEvents, event *SweepPoint, prev, next *SweepNode) bool { // a and b are always left-endpoints and a is below b //pair := SweepPointPair{a, b} //if _, ok := handled[pair]; ok { // return //} else if _, ok := handled[pair.Swapped()]; ok { // return //} //handled[pair] = struct{}{} var a, b *SweepPoint if prev == nil { a, b = event, next.SweepPoint } else if next == nil { a, b = prev.SweepPoint, event } else { a, b = prev.SweepPoint, next.SweepPoint } // find all intersections between segment pair // this returns either no intersections, or one or more secant/tangent intersections, // or exactly two "same" intersections which occurs when the segments overlap. zs = intersectionLineLineBentleyOttmann(zs[:0], a.Vector2, a.other.Vector2, b.Vector2, b.other.Vector2) // no (valid) intersections if len(zs) == 0 { return false } // Non-vertical but downwards-sloped segments may become vertical upon intersection due to // floating-point rounding and limited precision. Only the first segment of b can ever become // vertical, never the first segment of a: // - a and b may be segments in status when processing a right-endpoint. The left-endpoints of // both thus must be to the left of this right-endpoint (unless vertical) and can never // become vertical in their first segment. // - a is the segment of the currently processed left-endpoint and b is in status and above it. // a's left-endpoint is to the right of b's left-endpoint and is below b, thus: // - a and b go upwards: a nor b may become vertical, no reversal // - a goes downwards and b upwards: no intersection // - a goes upwards and b downwards: only a may become vertical but no reversal // - a and b go downwards: b may pass a's left-endpoint to its left (no intersection), // through it (tangential intersection, no splitting), or to its right so that a never // becomes vertical and thus no reversal // - b is the segment of the currently processed left-endpoint and a is in status and below it. // a's left-endpoint is below or to the left of b's left-endpoint and a is below b, thus: // - a and b go upwards: only a may become vertical, no reversal // - a goes downwards and b upwards: no intersection // - a goes upwards and b downwards: both may become vertical where only b must be reversed // - a and b go downwards: if b passes through a's left-endpoint, it must become vertical and // be reversed, or it passed to the right of a's left-endpoint and a nor b become vertical // Conclusion: either may become vertical, but only b ever needs reversal of direction. And // note that b is the currently processed left-endpoint and thus isn't in status. // Note: handle overlapping segments immediately by checking up and down status for segments // that compare equally with weak ordering (ie. overlapping). if !event.left { // intersection may be to the left (or below) the current event due to floating-point // precision which would interfere with the sequence in queue, this is a problem when // handling right-endpoints for i := range zs { zold := zs[i] z := &zs[i] if z.X < event.X { z.X = event.X } else if z.X == event.X && z.Y < event.Y { z.Y = event.Y } aMaxY := math32.Max(a.Y, a.other.Y) bMaxY := math32.Max(b.Y, b.other.Y) if a.other.X < z.X || b.other.X < z.X || aMaxY < z.Y || bMaxY < z.Y { fmt.Println("WARNING: intersection moved outside of segment:", zold, "=>", z) } } } // split segments a and b, but first find overlapping segments above and below and split them at the same point // this prevents a case that causes alternating intersections between overlapping segments and thus slowdown significantly //if a.node != nil { // splitOverlappingAtIntersections(zs, queue, a, true) //} aChanged := splitAtIntersections(zs, queue, a, true) //if b.node != nil { // splitOverlappingAtIntersections(zs, queue, b, false) //} bChanged := splitAtIntersections(zs, queue, b, false) return aChanged || bChanged } //func splitOverlappingAtIntersections(zs []Point, queue *SweepEvents, s *SweepPoint, isA bool) bool { // changed := false // for prev := s.node.Prev(); prev != nil; prev = prev.Prev() { // if prev.Point == s.Point && prev.other.Point == s.other.Point { // splitAtIntersections(zs, queue, prev.SweepPoint, isA) // changed = true // } // } // if !changed { // for next := s.node.Next(); next != nil; next = next.Next() { // if next.Point == s.Point && next.other.Point == s.other.Point { // splitAtIntersections(zs, queue, next.SweepPoint, isA) // changed = true // } // } // } // return changed //} func splitAtIntersections(zs []math32.Vector2, queue *SweepEvents, s *SweepPoint, isA bool) bool { changed := false for i := len(zs) - 1; 0 <= i; i-- { z := zs[i] if z == s.Vector2 || z == s.other.Vector2 { // ignore tangent intersections at the endpoints continue } // split segment at intersection right, left := s.SplitAt(z) // reverse direction if necessary if left.X == left.other.X { // segment after the split is vertical left.vertical, left.other.vertical = true, true if left.other.Y < left.Y { left.Reverse() } } else if right.X == right.other.X { // segment before the split is vertical right.vertical, right.other.vertical = true, true if right.Y < right.other.Y { // reverse first segment if isA { fmt.Println("WARNING: reversing first segment of A") } if right.other.node != nil { // panic("impossible: first segment became vertical and needs reversal, but was already in the sweep status") continue } right.Reverse() // Note that we swap the content of the currently processed left-endpoint of b with // the new left-endpoint vertically below. The queue may not be strictly ordered // with other vertical segments at the new left-endpoint, but this isn't a problem // since we sort the events in each square after the Bentley-Ottmann phase. // update references from handled and queue by swapping their contents first := right.other *right, *first = *first, *right first.other, right.other = right, first } } // add to handled //handled[SweepPointPair{a, bLeft}] = struct{}{} //if aPrevLeft != a { // // there is only one non-tangential intersection // handled[SweepPointPair{aPrevLeft, bLeft}] = struct{}{} //} // add to queue queue.Push(right) queue.Push(left) changed = true } return changed } //func reorderStatus(queue *SweepEvents, event *SweepPoint, aOld, bOld *SweepNode) { // var aNew, bNew *SweepNode // var aMove, bMove int // if aOld != nil { // // a == prev is a node in status that needs to be reordered // aNew, aMove = aOld.fix() // } // if bOld != nil { // // b == next is a node in status that needs to be reordered // bNew, bMove = bOld.fix() // } // // // find new intersections after snapping and moving around, first between the (new) neighbours // // of a and b, and then check if any other segment became adjacent due to moving around a or b, // // while avoiding superfluous checking for intersections (the aMove/bMove conditions) // if aNew != nil { // if prev := aNew.Prev(); prev != nil && aMove != bMove+1 { // // b is not a's previous // addIntersections(queue, event, prev, aNew) // } // if next := aNew.Next(); next != nil && aMove != bMove-1 { // // b is not a's next // addIntersections(queue, event, aNew, next) // } // } // if bNew != nil { // if prev := bNew.Prev(); prev != nil && bMove != aMove+1 { // // a is not b's previous // addIntersections(queue, event, prev, bNew) // } // if next := bNew.Next(); next != nil && bMove != aMove-1 { // // a is not b's next // addIntersections(queue, event, bNew, next) // } // } // if aOld != nil && aMove != 0 && bMove != -1 { // // a's old position is not aNew or bNew // if prev := aOld.Prev(); prev != nil && aMove != -1 && bMove != -2 { // // a nor b are not old a's previous // addIntersections(queue, event, prev, aOld) // } // if next := aOld.Next(); next != nil && aMove != 1 && bMove != 0 { // // a nor b are not old a's next // addIntersections(queue, event, aOld, next) // } // } // if bOld != nil && aMove != 1 && bMove != 0 { // // b's old position is not aNew or bNew // if aOld == nil { // if prev := bOld.Prev(); prev != nil && aMove != 0 && bMove != -1 { // // a nor b are not old b's previous // addIntersections(queue, event, prev, bOld) // } // } // if next := bOld.Next(); next != nil && aMove != 2 && bMove != 1 { // // a nor b are not old b's next // addIntersections(queue, event, bOld, next) // } // } //} type toleranceSquare struct { X, Y float32 // snapped value Events []*SweepPoint // all events in this square // reference node inside or near the square // after breaking up segments, this is the previous node (ie. completely below the square) Node *SweepNode // lower and upper node crossing this square Lower, Upper *SweepNode } type toleranceSquares []*toleranceSquare func (squares *toleranceSquares) find(x, y float32) (int, bool) { // find returns the index of the square at or above (x,y) (or len(squares) if above all) // the bool indicates if the square exists, otherwise insert a new square at that index for i := len(*squares) - 1; 0 <= i; i-- { if (*squares)[i].X < x || (*squares)[i].Y < y { return i + 1, false } else if (*squares)[i].Y == y { return i, true } } return 0, false } func (squares *toleranceSquares) Add(x float32, event *SweepPoint, refNode *SweepNode) { // refNode is always the node itself for left-endpoints, and otherwise the previous node (ie. // the node below) of a right-endpoint, or the next (ie. above) node if the previous is nil. // It may be inside or outside the right edge of the square. If outside, it is the first such // segment going upwards/downwards from the square (and not just any segment). y := snap(event.Y, BentleyOttmannEpsilon) if idx, ok := squares.find(x, y); !ok { // create new tolerance square square := boSquarePool.Get().(*toleranceSquare) *square = toleranceSquare{ X: x, Y: y, Events: []*SweepPoint{event}, Node: refNode, } *squares = append((*squares)[:idx], append(toleranceSquares{square}, (*squares)[idx:]...)...) } else { // insert into existing tolerance square (*squares)[idx].Node = refNode (*squares)[idx].Events = append((*squares)[idx].Events, event) } // (nearly) vertical segments may still be used as the reference segment for squares around // in that case, replace with the new reference node (above or below that segment) if !event.left { orig := event.other.node for i := len(*squares) - 1; 0 <= i && (*squares)[i].X == x; i-- { if (*squares)[i].Node == orig { (*squares)[i].Node = refNode } } } } //func (event *SweepPoint) insertIntoSortedH(events *[]*SweepPoint) { // // O(log n) // lo, hi := 0, len(*events) // for lo < hi { // mid := (lo + hi) / 2 // if (*events)[mid].LessH(event, false) { // lo = mid + 1 // } else { // hi = mid // } // } // // sorted := sort.IsSorted(eventSliceH(*events)) // if !sorted { // fmt.Println("WARNING: H not sorted") // for i, event := range *events { // fmt.Println(i, event, event.Angle()) // } // } // *events = append(*events, nil) // copy((*events)[lo+1:], (*events)[lo:]) // (*events)[lo] = event // if sorted && !sort.IsSorted(eventSliceH(*events)) { // fmt.Println("ERROR: not sorted after inserting into events:", *events) // } //} func (event *SweepPoint) breakupSegment(events *[]*SweepPoint, index int, x, y float32) *SweepPoint { // break up a segment in two parts and let the middle point be (x,y) if snap(event.X, BentleyOttmannEpsilon) == x && snap(event.Y, BentleyOttmannEpsilon) == y || snap(event.other.X, BentleyOttmannEpsilon) == x && snap(event.other.Y, BentleyOttmannEpsilon) == y { // segment starts or ends in tolerance square, don't break up return event } // original segment should be kept in-place to not alter the queue or status r, l := event.SplitAt(math32.Vector2{x, y}) r.index, l.index = index, index // reverse //if r.other.X == r.X { // if l.other.Y < r.other.Y { // r.Reverse() // } // r.vertical, r.other.vertical = true, true //} else if l.other.X == l.X { // if l.other.Y < r.other.Y { // l.Reverse() // } // l.vertical, l.other.vertical = true, true //} // update node reference if event.node != nil { l.node, event.node = event.node, nil l.node.SweepPoint = l } *events = append(*events, r, l) return l } func (squares toleranceSquares) breakupCrossingSegments(n int, x float32) { // find and break up all segments that cross this tolerance square // note that we must move up to find all upwards-sloped segments and then move down for the // downwards-sloped segments, since they may need to be broken up in other squares first x0, x1 := x-BentleyOttmannEpsilon/2.0, x+BentleyOttmannEpsilon/2.0 // scan squares bottom to top for i := n; i < len(squares); i++ { square := squares[i] // pointer // be aware that a tolerance square is inclusive of the left and bottom edge // and only the bottom-left corner yTop, yBottom := square.Y+BentleyOttmannEpsilon/2.0, square.Y-BentleyOttmannEpsilon/2.0 // from reference node find the previous/lower/upper segments for this square // the reference node may be any of the segments that cross the right-edge of the square, // or a segment below or above the right-edge of the square if square.Node != nil { y0, y1 := square.Node.ToleranceEdgeY(x0, x1) below, above := y0 < yBottom && y1 <= yBottom, yTop <= y0 && yTop <= y1 if !below && !above { // reference node is inside the square square.Lower, square.Upper = square.Node, square.Node } // find upper node if !above { for next := square.Node.Next(); next != nil; next = next.Next() { y0, y1 := next.ToleranceEdgeY(x0, x1) if yTop <= y0 && yTop <= y1 { // above break } else if y0 < yBottom && y1 <= yBottom { // below square.Node = next continue } square.Upper = next if square.Lower == nil { // this is set if the reference node is below the square square.Lower = next } } } // find lower node and set reference node to the node completely below the square if !below { prev := square.Node.Prev() for ; prev != nil; prev = prev.Prev() { y0, y1 := prev.ToleranceEdgeY(x0, x1) if y0 < yBottom && y1 <= yBottom { // exclusive for bottom-right corner // below break } else if yTop <= y0 && yTop <= y1 { // above square.Node = prev continue } square.Lower = prev if square.Upper == nil { // this is set if the reference node is above the square square.Upper = prev } } square.Node = prev } } // find all segments that cross the tolerance square // first find all segments that extend to the right (they are in the sweepline status) if square.Lower != nil { for node := square.Lower; ; node = node.Next() { node.breakupSegment(&squares[i].Events, i, x, square.Y) if node == square.Upper { break } } } // then find which segments that end in this square go through other squares for _, event := range square.Events { if !event.left { y0, _ := event.ToleranceEdgeY(x0, x1) s := event.other if y0 < yBottom { // comes from below, find lowest square and breakup in each square j0 := i for j := i - 1; 0 <= j; j-- { if squares[j].X != x || squares[j].Y+BentleyOttmannEpsilon/2.0 <= y0 { break } j0 = j } for j := j0; j < i; j++ { s = s.breakupSegment(&squares[j].Events, j, x, squares[j].Y) } } else if yTop <= y0 { // comes from above, find highest square and breakup in each square j0 := i for j := i + 1; j < len(squares); j++ { if y0 < squares[j].Y-BentleyOttmannEpsilon/2.0 { break } j0 = j } for j := j0; i < j; j-- { s = s.breakupSegment(&squares[j].Events, j, x, squares[j].Y) } } } } } } type eventSliceV []*SweepPoint func (a eventSliceV) Len() int { return len(a) } func (a eventSliceV) Less(i, j int) bool { return a[i].CompareV(a[j]) < 0 } func (a eventSliceV) Swap(i, j int) { a[i].node.SweepPoint, a[j].node.SweepPoint = a[j], a[i] a[i].node, a[j].node = a[j].node, a[i].node a[i], a[j] = a[j], a[i] } type eventSliceH []*SweepPoint func (a eventSliceH) Len() int { return len(a) } func (a eventSliceH) Less(i, j int) bool { return a[i].LessH(a[j]) } func (a eventSliceH) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (cur *SweepPoint) computeSweepFields(prev *SweepPoint, op pathOp, fillRule ppath.FillRules) { // cur is left-endpoint if !cur.open { cur.selfWindings = 1 if !cur.increasing { cur.selfWindings = -1 } } // skip vertical segments cur.prev = prev for prev != nil && prev.vertical { prev = prev.prev } // compute windings if prev != nil { if cur.clipping == prev.clipping { cur.windings = prev.windings + prev.selfWindings cur.otherWindings = prev.otherWindings + prev.otherSelfWindings } else { cur.windings = prev.otherWindings + prev.otherSelfWindings cur.otherWindings = prev.windings + prev.selfWindings } } else { // may have been copied when intersected / broken up cur.windings, cur.otherWindings = 0, 0 } cur.inResult = cur.InResult(op, fillRule) cur.other.inResult = cur.inResult } func (s *SweepPoint) InResult(op pathOp, fillRule ppath.FillRules) uint8 { lowerWindings, lowerOtherWindings := s.windings, s.otherWindings upperWindings, upperOtherWindings := s.windings+s.selfWindings, s.otherWindings+s.otherSelfWindings if s.clipping { lowerWindings, lowerOtherWindings = lowerOtherWindings, lowerWindings upperWindings, upperOtherWindings = upperOtherWindings, upperWindings } if s.open { // handle open paths on the subject switch op { case opSettle, opOR, opDIV: return 1 case opAND: if fillRule.Fills(lowerOtherWindings) || fillRule.Fills(upperOtherWindings) { return 1 } case opNOT, opXOR: if !fillRule.Fills(lowerOtherWindings) || !fillRule.Fills(upperOtherWindings) { return 1 } } return 0 } // lower/upper windings refers to subject path, otherWindings to clipping path var belowFills, aboveFills bool switch op { case opSettle: belowFills = fillRule.Fills(lowerWindings) aboveFills = fillRule.Fills(upperWindings) case opAND: belowFills = fillRule.Fills(lowerWindings) && fillRule.Fills(lowerOtherWindings) aboveFills = fillRule.Fills(upperWindings) && fillRule.Fills(upperOtherWindings) case opOR: belowFills = fillRule.Fills(lowerWindings) || fillRule.Fills(lowerOtherWindings) aboveFills = fillRule.Fills(upperWindings) || fillRule.Fills(upperOtherWindings) case opNOT: belowFills = fillRule.Fills(lowerWindings) && !fillRule.Fills(lowerOtherWindings) aboveFills = fillRule.Fills(upperWindings) && !fillRule.Fills(upperOtherWindings) case opXOR: belowFills = fillRule.Fills(lowerWindings) != fillRule.Fills(lowerOtherWindings) aboveFills = fillRule.Fills(upperWindings) != fillRule.Fills(upperOtherWindings) case opDIV: belowFills = fillRule.Fills(lowerWindings) aboveFills = fillRule.Fills(upperWindings) if belowFills && aboveFills { return 2 } else if belowFills || aboveFills { return 1 } return 0 } // only keep edge if there is a change in filling between both sides if belowFills != aboveFills { return 1 } return 0 } func (s *SweepPoint) mergeOverlapping(op pathOp, fillRule ppath.FillRules) { // When merging overlapping segments, the order of the right-endpoints may have changed and // thus be different from the order used to compute the sweep fields, here we reset the values // for windings and otherWindings to be taken from the segment below (prev) which was updated // after snapping the endpoints. // We use event.overlapped to handle segments once and count windings once, in whichever order // the events are handled. We also update prev to reflect the segment below the overlapping // segments. if s.overlapped { // already handled return } prev := s.prev for ; prev != nil; prev = prev.prev { if prev.overlapped || s.Vector2 != prev.Vector2 || s.other.Vector2 != prev.other.Vector2 { break } // combine selfWindings if s.clipping == prev.clipping { s.selfWindings += prev.selfWindings s.otherSelfWindings += prev.otherSelfWindings } else { s.selfWindings += prev.otherSelfWindings s.otherSelfWindings += prev.selfWindings } prev.windings, prev.selfWindings, prev.otherWindings, prev.otherSelfWindings = 0, 0, 0, 0 prev.inResult, prev.other.inResult = 0, 0 prev.overlapped = true } if prev == s.prev { return } // compute merged windings if prev == nil { s.windings, s.otherWindings = 0, 0 } else if s.clipping == prev.clipping { s.windings = prev.windings + prev.selfWindings s.otherWindings = prev.otherWindings + prev.otherSelfWindings } else { s.windings = prev.otherWindings + prev.otherSelfWindings s.otherWindings = prev.windings + prev.selfWindings } s.inResult = s.InResult(op, fillRule) s.other.inResult = s.inResult s.prev = prev } func bentleyOttmann(ps, qs ppath.Paths, op pathOp, fillRule ppath.FillRules) ppath.Path { // TODO: make public and add grid spacing argument // TODO: support OpDIV, keeping only subject, or both subject and clipping subpaths // TODO: add Intersects/Touches functions (return bool) // TODO: add Intersections function (return []Point) // TODO: support Cut to cut a path in subpaths between intersections (not polygons) // TODO: support elliptical arcs // TODO: use a red-black tree for the sweepline status? // TODO: use a red-black or 2-4 tree for the sweepline queue (LessH is 33% of time spent now), // perhaps a red-black tree where the nodes are min-queues of the resulting squares // TODO: optimize path data by removing commands, set number of same command (50% less memory) // TODO: can we get idempotency (same result after second time) by tracing back each snapped // right-endpoint for the squares it may now intersect? (Hershberger 2013) // Implementation of the Bentley-Ottmann algorithm by reducing the complexity of finding // intersections to O((n + k) log n), with n the number of segments and k the number of // intersections. All special cases are handled by use of: // - M. de Berg, et al., "Computational Geometry", Chapter 2, DOI: 10.1007/978-3-540-77974-2 // - F. Martínez, et al., "A simple algorithm for Boolean operations on polygons", Advances in // Engineering Software 64, p. 11-19, 2013, DOI: 10.1016/j.advengsoft.2013.04.004 // - J. Hobby, "Practical segment intersection with finite precision output", Computational // Geometry, 1997 // - J. Hershberger, "Stable snap rounding", Computational Geometry: Theory and Applications, // 2013, DOI: 10.1016/j.comgeo.2012.02.011 // - https://github.com/verven/contourklip // Bentley-Ottmann is the most popular algorithm to find path intersections, which is mainly // due to it's relative simplicity and the fact that it is (much) faster than the naive // approach. It however does not specify how special cases should be handled (overlapping // segments, multiple segment endpoints in one point, vertical segments), which is treated in // later works by other authors (e.g. Martínez from which this implementation draws // inspiration). I've made some small additions and adjustments to make it work in all cases // I encountered. Specifically, this implementation has the following properties: // - Subject and clipping paths may consist of any number of contours / subpaths. // - Any contour may be oriented clockwise (CW) or counter-clockwise (CCW). // - Any path or contour may self-intersect any number of times. // - Any point may be crossed multiple times by any path. // - Segments may overlap any number of times by any path. // - Segments may be vertical. // - The clipping path is implicitly closed, it makes no sense if it is an open path. // - The subject path is currently implicitly closed, but it is WIP to support open paths. // - ppath.Paths are currently flattened, but supporting Bézier or elliptical arcs is a WIP. // An unaddressed problem in those works is that of numerical accuracies. The main problem is // that calculating the intersections is not precise; the imprecision of the initial endpoints // of a path can be trivially fixed before the algorithm. Intersections however are calculated // during the algorithm and must be addressed. There are a few authors that propose a solution, // and Hobby's work inspired this implementation. The approach taken is somewhat different // though: // - Instead of integers (or rational numbers implemented using integers), floating points are // used for their speed. It isn't even necessary that the grid points can be represented // exactly in the floating point format, as long as all points in the tolerance square around // the grid points snap to the same point. Now we can compare using == instead of an equality // test. // - As in Martínez, we treat an intersection as a right- and left-endpoint combination and not // as a third type of event. This avoids rearrangement of events in the sweep status as it is // removed and reinserted into the right position, but at the cost of more delete/insert // operations in the sweep status (potential to improve performance). // - As we run the Bentley-Ottmann algorithm, found endpoints must also be snapped to the grid. // Since intersections are found in advance (ie. towards the right), we have no idea how the // sweepline status will be yet, so we cannot snap those intersections to the grid yet. We // must snap all endpoints/intersections when we reach them (ie. pop them off the queue). // When we get to an endpoint, snap all endpoints in the tolerance square around the grid // point to that point, and process all endpoints and intersections. Additionally, we should // break-up all segments that pass through the square into two, and snap them to the grid // point as well. These segments pass very close to another endpoint, and by snapping those // to the grid we avoid the problem where we may or may not find that the segment intersects. // - Note that most (not all) intersections on the right are calculated with the left-endpoint // already snapped, which may move the intersection to another grid point. These inaccuracies // depend on the grid spacing and can be made small relative to the size of the input paths. // // The difference with Hobby's steps is that we advance Bentley-Ottmann for the entire column, // and only then do we calculate crossing segments. I'm not sure what reason Hobby has to do // this in two fases. Also, Hobby uses a shadow sweep line status structure which contains the // segments sorted after snapping. Instead of using two sweep status structures (the original // Bentley-Ottmann and the shadow with snapped segments), we sort the status after each column. // Additionally, we need to keep the sweep line queue structure ordered as well for the result // polygon (instead of the queue we gather the events for each sqaure, and sort those), and we // need to calculate the sweep fields for the result polygon. // // It is best to think of processing the tolerance squares, one at a time moving bottom-to-top, // for each column while moving the sweepline from left to right. Since all intersections // in this implementation are already converted to two right-endpoints and two left-endpoints, // we do all the snapping after each column and snapping the endpoints beforehand is not // necessary. We pop off all events from the queue that belong to the same column and process // them as we would with Bentley-Ottmann. This ensures that we find all original locations of // the intersections (except for intersections between segments in the sweep status structure // that are not yet adjacent, see note above) and may introduce new tolerance squares. For each // square, we find all segments that pass through and break them up and snap them to the grid. // Then snap all endpoints in the // square to the grid. We must sort the sweep line status and all events per square to account // for the new order after snapping. Some implementation observations: // - We must breakup segments that cross the square BEFORE we snap the square's endpoints, // since we depend on the order of in the sweep status (from after processing the column // using the original Bentley-Ottmann sweep line) for finding crossing segments. // - We find all original locations of intersections for adjacent segments during and after // processing the column. However, if intersections become adjacent later on, the // left-endpoint has already been snapped and the intersection has moved. // - We must be careful with overlapping segments. Since gridsnapping may introduce new // overlapping segments (potentially vertical), we must check for that when processing the // right-endpoints of each square. // // We thus proceed as follows: // - Process all events from left-to-right in a column using the regular Bentley-Ottmann. // - Identify all "hot" squares (those that contain endpoints / intersections). // - Find all segments that pass through each hot square, break them up and snap to the grid. // These may be segments that start left of the column and end right of it, but also segments // that start or end inside the column, or even start AND end inside the column (eg. vertical // or almost vertical segments). // - Snap all endpoints and intersections to the grid. // - Compute sweep fields / windings for all new left-endpoints. // - Handle segments that are now overlapping for all right-endpoints. // Note that we must be careful with vertical segments. boInitPoolsOnce() // use pools for SweepPoint and SweepNode to amortize repeated calls to BO // return in case of one path is empty if op == opSettle { qs = nil } else if qs.Empty() { if op == opAND { return ppath.Path{} } return SettlePaths(ps, fillRule) } if ps.Empty() { if qs != nil && (op == opOR || op == opXOR) { return SettlePaths(qs, fillRule) } return ppath.Path{} } // ensure that X-monotone property holds for Béziers and arcs by breaking them up at their // extremes along X (ie. their inflection points along X) // TODO: handle Béziers and arc segments //p = p.XMonotone() //q = q.XMonotone() for i, iMax := 0, len(ps); i < iMax; i++ { split := ps[i].Split() if 1 < len(split) { ps[i] = split[0] ps = append(ps, split[1:]...) } } for i := range ps { ps[i] = Flatten(ps[i], ppath.Tolerance) } if qs != nil { for i, iMax := 0, len(qs); i < iMax; i++ { split := qs[i].Split() if 1 < len(split) { qs[i] = split[0] qs = append(qs, split[1:]...) } } for i := range qs { qs[i] = Flatten(qs[i], ppath.Tolerance) } } // check for path bounding boxes to overlap // TODO: cluster paths that overlap and treat non-overlapping clusters separately, this // makes the algorithm "more linear" R := ppath.Path{} var pOverlaps, qOverlaps []bool if qs != nil { pBounds := make([]math32.Box2, len(ps)) qBounds := make([]math32.Box2, len(qs)) for i := range ps { pBounds[i] = ps[i].FastBounds() } for i := range qs { qBounds[i] = qs[i].FastBounds() } pOverlaps = make([]bool, len(ps)) qOverlaps = make([]bool, len(qs)) for i := range ps { for j := range qs { if touches(pBounds[i], qBounds[j]) { pOverlaps[i] = true qOverlaps[j] = true } } if !pOverlaps[i] && (op == opOR || op == opXOR || op == opNOT) { // path bounding boxes do not overlap, thus no intersections R = R.Append(Settle(ps[i], fillRule)) } } for j := range qs { if !qOverlaps[j] && (op == opOR || op == opXOR) { // path bounding boxes do not overlap, thus no intersections R = R.Append(Settle(qs[j], fillRule)) } } } // construct the priority queue of sweep events pSeg, qSeg := 0, 0 queue := &SweepEvents{} for i := range ps { if qs == nil || pOverlaps[i] { pSeg = queue.AddPathEndpoints(ps[i], pSeg, false) } } if qs != nil { for i := range qs { if qOverlaps[i] { // implicitly close all subpaths on Q if !qs[i].Closed() { qs[i].Close() } qSeg = queue.AddPathEndpoints(qs[i], qSeg, true) } } } queue.Init() // sort from left to right // run sweep line left-to-right zs := make([]math32.Vector2, 0, 2) // buffer for intersections centre := &SweepPoint{} // allocate here to reduce allocations events := []*SweepPoint{} // buffer used for ordering status status := &SweepStatus{} // contains only left events squares := toleranceSquares{} // sorted vertically, squares and their events // TODO: use linked list for toleranceSquares? for 0 < len(*queue) { // TODO: skip or stop depending on operation if we're to the left/right of subject/clipping polygon // We slightly divert from the original Bentley-Ottmann and paper implementation. First // we find the top element in queue but do not pop it off yet. If it is a right-event, pop // from queue and proceed as usual, but if it's a left-event we first check (and add) all // surrounding intersections to the queue. This may change the order from which we should // pop off the queue, since intersections may create right-events, or new left-events that // are lower (by compareTangentV). If no intersections are found, pop off the queue and // proceed as usual. // Pass 1 // process all events of the current column n := len(squares) x := snap(queue.Top().X, BentleyOttmannEpsilon) BentleyOttmannLoop: for 0 < len(*queue) && snap(queue.Top().X, BentleyOttmannEpsilon) == x { event := queue.Top() // TODO: breaking intersections into two right and two left endpoints is not the most // efficient. We could keep an intersection-type event and simply swap the order of the // segments in status (note there can be multiple segments crossing in one point). This // would alleviate a 2*m*log(n) search in status to remove/add the segments (m number // of intersections in one point, and n number of segments in status), and instead use // an m/2 number of swap operations. This alleviates pressure on the CompareV method. if !event.left { queue.Pop() n := event.other.node if n == nil { // panic("right-endpoint not part of status, probably buggy intersection code") // don't put back in boPointPool, rare event continue } else if n.SweepPoint == nil { // this may happen if the left-endpoint is to the right of the right-endpoint // for some reason, usually due to a bug in the segment intersection code // panic("other endpoint already removed, probably buggy intersection code") // don't put back in boPointPool, rare event continue } // find intersections between the now adjacent segments prev := n.Prev() next := n.Next() if prev != nil && next != nil { addIntersections(zs, queue, event, prev, next) } // add event to tolerance square if prev != nil { squares.Add(x, event, prev) } else { // next can be nil squares.Add(x, event, next) } // remove event from sweep status status.Remove(n) } else { // add intersections to queue prev, next := status.FindPrevNext(event) if prev != nil { addIntersections(zs, queue, event, prev, nil) } if next != nil { addIntersections(zs, queue, event, nil, next) } if queue.Top() != event { // check if the queue order was changed, this happens if the current event // is the left-endpoint of a segment that intersects with an existing segment // that goes below, or when two segments become fully overlapping, which sets // their order in status differently than when one of them extends further continue } queue.Pop() // add event to sweep status n := status.InsertAfter(prev, event) // add event to tolerance square squares.Add(x, event, n) } } // Pass 2 // find all crossing segments, break them up and snap to the grid squares.breakupCrossingSegments(n, x) // snap events to grid // note that this may make segments overlapping from the left and towards the right // we handle the former below, but ignore the latter which may result in overlapping // segments not being strictly ordered for j := n; j < len(squares); j++ { del := 0 square := squares[j] // pointer for i := 0; i < len(square.Events); i++ { event := square.Events[i] event.index = j event.X, event.Y = x, square.Y other := Gridsnap(event.other.Vector2, BentleyOttmannEpsilon) if event.Vector2 == other { // remove collapsed segments, we aggregate them with `del` to improve performance when we have many // TODO: prevent creating these segments in the first place del++ } else { if 0 < del { for _, event := range square.Events[i-del : i] { if !event.left { boPointPool.Put(event.other) boPointPool.Put(event) } } square.Events = append(square.Events[:i-del], square.Events[i:]...) i -= del del = 0 } if event.X == other.X { // correct for segments that have become vertical due to snap/breakup event.vertical, event.other.vertical = true, true if !event.left && event.Y < other.Y { // downward sloped, reverse direction event.Reverse() } } } } if 0 < del { for _, event := range square.Events[len(square.Events)-del:] { if !event.left { boPointPool.Put(event.other) boPointPool.Put(event) } } square.Events = square.Events[:len(square.Events)-del] } } for _, square := range squares[n:] { // reorder sweep status and events for result polygon // note that the number of events/nodes is usually small // and note that we must first snap all segments in this column before sorting if square.Lower != nil { events = events[:0] for n := square.Lower; ; n = n.Next() { events = append(events, n.SweepPoint) if n == square.Upper { break } } // TODO: test this thoroughly, this below prevents long loops of moving intersections to columns on the right for n := square.Lower; n != square.Upper; { next := n.Next() if 0 < n.CompareV(next.SweepPoint) { if next.other.X < n.other.X { r, l := n.SplitAt(next.other.Vector2) queue.Push(r) queue.Push(l) } else if n.other.X < next.other.X { r, l := next.SplitAt(n.other.Vector2) queue.Push(r) queue.Push(l) } } n = next } // keep unsorted events in the same slice n := len(events) events = append(events, events...) origEvents := events[n:] events = events[:n] sort.Sort(eventSliceV(events)) // find intersections between neighbouring segments due to snapping // TODO: ugly! has := false centre.Vector2 = math32.Vector2{square.X, square.Y} if prev := square.Lower.Prev(); prev != nil { has = addIntersections(zs, queue, centre, prev, square.Lower) } if next := square.Upper.Next(); next != nil { has = has || addIntersections(zs, queue, centre, square.Upper, next) } // find intersections between new neighbours in status after sorting for i, event := range events[:len(events)-1] { if event != origEvents[i] { n := event.node var j int for origEvents[j] != event { j++ } if next := n.Next(); next != nil && (j == 0 || next.SweepPoint != origEvents[j-1]) && (j+1 == len(origEvents) || next.SweepPoint != origEvents[j+1]) { // segment changed order and the segment above was not its neighbour has = has || addIntersections(zs, queue, centre, n, next) } } } if 0 < len(*queue) && snap(queue.Top().X, BentleyOttmannEpsilon) == x { //fmt.Println("WARNING: new intersections in this column!") goto BentleyOttmannLoop // TODO: is this correct? seems to work // TODO: almost parallel combined with overlapping segments may create many intersections considering order of // of overlapping segments and snapping after each column } else if has { // sort overlapping segments again // this is needed when segments get cut and now become equal to the adjacent // overlapping segments // TODO: segments should be sorted by segment ID when overlapping, even if // one segment extends further than the other, is that due to floating // point accuracy? sort.Sort(eventSliceV(events)) } } slices.SortFunc(square.Events, (*SweepPoint).CompareH) // compute sweep fields on left-endpoints for i, event := range square.Events { if !event.left { event.other.mergeOverlapping(op, fillRule) } else if event.node == nil { // vertical if 0 < i && square.Events[i-1].left { // against last left-endpoint in square // inside this square there are no crossing segments, they have been broken // up and have their left-endpoints sorted event.computeSweepFields(square.Events[i-1], op, fillRule) } else { // against first segment below square // square.Node may be nil var s *SweepPoint if square.Node != nil { s = square.Node.SweepPoint } event.computeSweepFields(s, op, fillRule) } } else { var s *SweepPoint if event.node.Prev() != nil { s = event.node.Prev().SweepPoint } event.computeSweepFields(s, op, fillRule) } } } } status.Clear() // release all nodes (but not SweepPoints) // build resulting polygons var Ropen ppath.Path for _, square := range squares { for _, cur := range square.Events { if cur.inResult == 0 { continue } BuildPath: windings := 0 prev := cur.prev if op != opDIV && prev != nil { windings = prev.resultWindings } first := cur indexR := len(R) R.MoveTo(cur.X, cur.Y) cur.resultWindings = windings if !first.open { // we go to the right/top cur.resultWindings++ } cur.other.resultWindings = cur.resultWindings for { // find segments starting from other endpoint, find the other segment amongst // them, the next segment should be the next going CCW i0 := 0 nodes := squares[cur.other.index].Events for i := range nodes { if nodes[i] == cur.other { i0 = i break } } // find the next segment in CW order, this will make smaller subpaths // instead one large path when multiple segments end at the same position var next *SweepPoint for i := i0 - 1; ; i-- { if i < 0 { i += len(nodes) } if i == i0 { break } else if 0 < nodes[i].inResult && nodes[i].open == first.open { next = nodes[i] break } } if next == nil { if first.open { R.LineTo(cur.other.X, cur.other.Y) } else { // fmt.Println(ps) // fmt.Println(op) // fmt.Println(qs) // panic("next node for result polygon is nil, probably buggy intersection code") } break } else if next == first { break // contour is done } cur = next R.LineTo(cur.X, cur.Y) cur.resultWindings = windings if cur.left && !first.open { // we go to the right/top cur.resultWindings++ } cur.other.resultWindings = cur.resultWindings cur.other.inResult-- cur.inResult-- } first.other.inResult-- first.inResult-- if first.open { if Ropen != nil { start := (R[indexR:]).Reverse() R = append(R[:indexR], start...) R = append(R, Ropen...) Ropen = nil } else { for _, cur2 := range square.Events { if 0 < cur2.inResult && cur2.open { cur = cur2 Ropen = make(ppath.Path, len(R)-indexR-4) copy(Ropen, R[indexR+4:]) R = R[:indexR] goto BuildPath } } } } else { R.Close() if windings%2 != 0 { // orient holes clockwise hole := R[indexR:].Reverse() R = append(R[:indexR], hole...) } } } for _, event := range square.Events { if !event.left { boPointPool.Put(event.other) boPointPool.Put(event) } } boSquarePool.Put(square) } return R } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package intersect import ( "fmt" "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" ) // see https://github.com/signavio/svg-intersections // see https://github.com/w8r/bezier-intersect // see https://cs.nyu.edu/exact/doc/subdiv1.pdf // intersect for path segments a and b, starting at a0 and b0. // Note that all intersection functions return up to two intersections. func IntersectionSegment(zs Intersections, a0 math32.Vector2, a ppath.Path, b0 math32.Vector2, b ppath.Path) Intersections { n := len(zs) swapCurves := false if a[0] == ppath.LineTo || a[0] == ppath.Close { if b[0] == ppath.LineTo || b[0] == ppath.Close { zs = intersectionLineLine(zs, a0, math32.Vec2(a[1], a[2]), b0, math32.Vec2(b[1], b[2])) } else if b[0] == ppath.QuadTo { zs = intersectionLineQuad(zs, a0, math32.Vec2(a[1], a[2]), b0, math32.Vec2(b[1], b[2]), math32.Vec2(b[3], b[4])) } else if b[0] == ppath.CubeTo { zs = intersectionLineCube(zs, a0, math32.Vec2(a[1], a[2]), b0, math32.Vec2(b[1], b[2]), math32.Vec2(b[3], b[4]), math32.Vec2(b[5], b[6])) } else if b[0] == ppath.ArcTo { rx := b[1] ry := b[2] phi := b[3] large, sweep := ppath.ToArcFlags(b[4]) cx, cy, theta0, theta1 := ppath.EllipseToCenter(b0.X, b0.Y, rx, ry, phi, large, sweep, b[5], b[6]) zs = intersectionLineEllipse(zs, a0, math32.Vec2(a[1], a[2]), math32.Vector2{cx, cy}, math32.Vector2{rx, ry}, phi, theta0, theta1) } } else if a[0] == ppath.QuadTo { if b[0] == ppath.LineTo || b[0] == ppath.Close { zs = intersectionLineQuad(zs, b0, math32.Vec2(b[1], b[2]), a0, math32.Vec2(a[1], a[2]), math32.Vec2(a[3], a[4])) swapCurves = true } else if b[0] == ppath.QuadTo { panic("unsupported intersection for quad-quad") } else if b[0] == ppath.CubeTo { panic("unsupported intersection for quad-cube") } else if b[0] == ppath.ArcTo { panic("unsupported intersection for quad-arc") } } else if a[0] == ppath.CubeTo { if b[0] == ppath.LineTo || b[0] == ppath.Close { zs = intersectionLineCube(zs, b0, math32.Vec2(b[1], b[2]), a0, math32.Vec2(a[1], a[2]), math32.Vec2(a[3], a[4]), math32.Vec2(a[5], a[6])) swapCurves = true } else if b[0] == ppath.QuadTo { panic("unsupported intersection for cube-quad") } else if b[0] == ppath.CubeTo { panic("unsupported intersection for cube-cube") } else if b[0] == ppath.ArcTo { panic("unsupported intersection for cube-arc") } } else if a[0] == ppath.ArcTo { rx := a[1] ry := a[2] phi := a[3] large, sweep := ppath.ToArcFlags(a[4]) cx, cy, theta0, theta1 := ppath.EllipseToCenter(a0.X, a0.Y, rx, ry, phi, large, sweep, a[5], a[6]) if b[0] == ppath.LineTo || b[0] == ppath.Close { zs = intersectionLineEllipse(zs, b0, math32.Vec2(b[1], b[2]), math32.Vector2{cx, cy}, math32.Vector2{rx, ry}, phi, theta0, theta1) swapCurves = true } else if b[0] == ppath.QuadTo { panic("unsupported intersection for arc-quad") } else if b[0] == ppath.CubeTo { panic("unsupported intersection for arc-cube") } else if b[0] == ppath.ArcTo { rx2 := b[1] ry2 := b[2] phi2 := b[3] large2, sweep2 := ppath.ToArcFlags(b[4]) cx2, cy2, theta20, theta21 := ppath.EllipseToCenter(b0.X, b0.Y, rx2, ry2, phi2, large2, sweep2, b[5], b[6]) zs = intersectionEllipseEllipse(zs, math32.Vector2{cx, cy}, math32.Vector2{rx, ry}, phi, theta0, theta1, math32.Vector2{cx2, cy2}, math32.Vector2{rx2, ry2}, phi2, theta20, theta21) } } // swap A and B in the intersection found to match segments A and B of this function if swapCurves { for i := n; i < len(zs); i++ { zs[i].T[0], zs[i].T[1] = zs[i].T[1], zs[i].T[0] zs[i].Dir[0], zs[i].Dir[1] = zs[i].Dir[1], zs[i].Dir[0] } } return zs } // Intersection is an intersection between two path segments, e.g. Line x Line. Note that an // intersection is tangent also when it is at one of the endpoints, in which case it may be tangent // for this segment but may or may not cross the path depending on the adjacent segment. // Notabene: for quad/cube/ellipse aligned angles at the endpoint for non-overlapping curves are deviated slightly to correctly calculate the value for Into, and will thus not be aligned type Intersection struct { math32.Vector2 // coordinate of intersection T [2]float32 // position along segment [0,1] Dir [2]float32 // direction at intersection [0,2*pi) Tangent bool // intersection is tangent (touches) instead of secant (crosses) Same bool // intersection is of two overlapping segments (tangent is also true) } // Into returns true if first path goes into the left-hand side of the second path, // i.e. the second path goes to the right-hand side of the first path. func (z Intersection) Into() bool { return angleBetweenExclusive(z.Dir[1]-z.Dir[0], math32.Pi, 2.0*math32.Pi) } func (z Intersection) Equals(o Intersection) bool { return ppath.EqualPoint(z.Vector2, o.Vector2) && ppath.Equal(z.T[0], o.T[0]) && ppath.Equal(z.T[1], o.T[1]) && ppath.AngleEqual(z.Dir[0], o.Dir[0]) && ppath.AngleEqual(z.Dir[1], o.Dir[1]) && z.Tangent == o.Tangent && z.Same == o.Same } func (z Intersection) String() string { var extra string if z.Tangent { extra = " Tangent" } if z.Same { extra = " Same" } return fmt.Sprintf("({%v,%v} t={%v,%v} dir={%v°,%v°}%v)", numEps(z.Vector2.X), numEps(z.Vector2.Y), numEps(z.T[0]), numEps(z.T[1]), numEps(math32.RadToDeg(ppath.AngleNorm(z.Dir[0]))), numEps(math32.RadToDeg(ppath.AngleNorm(z.Dir[1]))), extra) } type Intersections []Intersection // Has returns true if there are secant/tangent intersections. func (zs Intersections) Has() bool { return 0 < len(zs) } // HasSecant returns true when there are secant intersections, i.e. the curves intersect and cross (they cut). func (zs Intersections) HasSecant() bool { for _, z := range zs { if !z.Tangent { return true } } return false } // HasTangent returns true when there are tangent intersections, i.e. the curves intersect but don't cross (they touch). func (zs Intersections) HasTangent() bool { for _, z := range zs { if z.Tangent { return true } } return false } func (zs Intersections) add(pos math32.Vector2, ta, tb, dira, dirb float32, tangent, same bool) Intersections { // normalise T values between [0,1] if ta < 0.0 { // || ppath.Equal(ta, 0.0) { ta = 0.0 } else if 1.0 <= ta { // || ppath.Equal(ta, 1.0) { ta = 1.0 } if tb < 0.0 { // || ppath.Equal(tb, 0.0) { tb = 0.0 } else if 1.0 < tb { // || ppath.Equal(tb, 1.0) { tb = 1.0 } return append(zs, Intersection{pos, [2]float32{ta, tb}, [2]float32{dira, dirb}, tangent, same}) } func correctIntersection(z, aMin, aMax, bMin, bMax math32.Vector2) math32.Vector2 { if z.X < aMin.X { //fmt.Println("CORRECT 1:", a0, a1, "--", b0, b1) z.X = aMin.X } else if aMax.X < z.X { //fmt.Println("CORRECT 2:", a0, a1, "--", b0, b1) z.X = aMax.X } if z.X < bMin.X { //fmt.Println("CORRECT 3:", a0, a1, "--", b0, b1) z.X = bMin.X } else if bMax.X < z.X { //fmt.Println("CORRECT 4:", a0, a1, "--", b0, b1) z.X = bMax.X } if z.Y < aMin.Y { //fmt.Println("CORRECT 5:", a0, a1, "--", b0, b1) z.Y = aMin.Y } else if aMax.Y < z.Y { //fmt.Println("CORRECT 6:", a0, a1, "--", b0, b1) z.Y = aMax.Y } if z.Y < bMin.Y { //fmt.Println("CORRECT 7:", a0, a1, "--", b0, b1) z.Y = bMin.Y } else if bMax.Y < z.Y { //fmt.Println("CORRECT 8:", a0, a1, "--", b0, b1) z.Y = bMax.Y } return z } // F. Antonio, "Faster Line Segment Intersection", Graphics Gems III, 1992 func intersectionLineLineBentleyOttmann(zs []math32.Vector2, a0, a1, b0, b1 math32.Vector2) []math32.Vector2 { // fast line-line intersection code, with additional constraints for the BentleyOttmann code: // - a0 is to the left and/or bottom of a1, same for b0 and b1 // - an intersection z must keep the above property between (a0,z), (z,a1), (b0,z), and (z,b1) // note that an exception is made for (z,a1) and (z,b1) to allow them to become vertical, this // is because there isn't always "space" between a0.X and a1.X, eg. when a1.X = nextafter(a0.X) if a1.X < b0.X || b1.X < a0.X { return zs } aMin, aMax, bMin, bMax := a0, a1, b0, b1 if a1.Y < a0.Y { aMin.Y, aMax.Y = aMax.Y, aMin.Y } if b1.Y < b0.Y { bMin.Y, bMax.Y = bMax.Y, bMin.Y } if aMax.Y < bMin.Y || bMax.Y < aMin.Y { return zs } else if (aMax.X == bMin.X || bMax.X == aMin.X) && (aMax.Y == bMin.Y || bMax.Y == aMin.Y) { return zs } // only the position and T values are valid for each intersection A := a1.Sub(a0) B := b0.Sub(b1) C := a0.Sub(b0) denom := B.Cross(A) // divide by length^2 since the perpdot between very small segments may be below Epsilon if denom == 0.0 { // colinear if C.Cross(B) == 0.0 { // overlap, rotate to x-axis a, b, c, d := a0.X, a1.X, b0.X, b1.X if math32.Abs(A.X) < math32.Abs(A.Y) { // mostly vertical a, b, c, d = a0.Y, a1.Y, b0.Y, b1.Y } if c < b && a < d { if a < c { zs = append(zs, b0) } else if c < a { zs = append(zs, a0) } if d < b { zs = append(zs, b1) } else if b < d { zs = append(zs, a1) } } } return zs } // find intersections within +-Epsilon to avoid missing near intersections ta := C.Cross(B) / denom if ta < -ppath.Epsilon || 1.0+ppath.Epsilon < ta { return zs } tb := A.Cross(C) / denom if tb < -ppath.Epsilon || 1.0+ppath.Epsilon < tb { return zs } // ta is snapped to 0.0 or 1.0 if very close if ta <= ppath.Epsilon { ta = 0.0 } else if 1.0-ppath.Epsilon <= ta { ta = 1.0 } z := a0.Lerp(a1, ta) z = correctIntersection(z, aMin, aMax, bMin, bMax) if z != a0 && z != a1 || z != b0 && z != b1 { // not at endpoints for both if a0 != b0 && z != a0 && z != b0 && b0.Sub(z).Cross(z.Sub(a0)) == 0.0 { a, c, m := a0.X, b0.X, z.X if math32.Abs(z.Sub(a0).X) < math32.Abs(z.Sub(a0).Y) { // mostly vertical a, c, m = a0.Y, b0.Y, z.Y } if a != c && (a < m) == (c < m) { if a < m && a < c || m < a && c < a { zs = append(zs, b0) } else { zs = append(zs, a0) } } zs = append(zs, z) } else if a1 != b1 && z != a1 && z != b1 && z.Sub(b1).Cross(a1.Sub(z)) == 0.0 { b, d, m := a1.X, b1.X, z.X if math32.Abs(z.Sub(a1).X) < math32.Abs(z.Sub(a1).Y) { // mostly vertical b, d, m = a1.Y, b1.Y, z.Y } if b != d && (b < m) == (d < m) { if b < m && b < d || m < b && d < b { zs = append(zs, b1) } else { zs = append(zs, a1) } } } else { zs = append(zs, z) } } return zs } func intersectionLineLine(zs Intersections, a0, a1, b0, b1 math32.Vector2) Intersections { if ppath.EqualPoint(a0, a1) || ppath.EqualPoint(b0, b1) { return zs // zero-length Close } da := a1.Sub(a0) db := b1.Sub(b0) anglea := ppath.Angle(da) angleb := ppath.Angle(db) div := da.Cross(db) // divide by length^2 since otherwise the perpdot between very small segments may be // below Epsilon if length := da.Length() * db.Length(); ppath.Equal(div/length, 0.0) { // parallel if ppath.Equal(b0.Sub(a0).Cross(db), 0.0) { // overlap, rotate to x-axis a := a0.Rot(-anglea, math32.Vector2{}).X b := a1.Rot(-anglea, math32.Vector2{}).X c := b0.Rot(-anglea, math32.Vector2{}).X d := b1.Rot(-anglea, math32.Vector2{}).X if inInterval(a, c, d) && inInterval(b, c, d) { // a-b in c-d or a-b == c-d zs = zs.add(a0, 0.0, (a-c)/(d-c), anglea, angleb, true, true) zs = zs.add(a1, 1.0, (b-c)/(d-c), anglea, angleb, true, true) } else if inInterval(c, a, b) && inInterval(d, a, b) { // c-d in a-b zs = zs.add(b0, (c-a)/(b-a), 0.0, anglea, angleb, true, true) zs = zs.add(b1, (d-a)/(b-a), 1.0, anglea, angleb, true, true) } else if inInterval(a, c, d) { // a in c-d same := a < d-ppath.Epsilon || a < c-ppath.Epsilon zs = zs.add(a0, 0.0, (a-c)/(d-c), anglea, angleb, true, same) if a < d-ppath.Epsilon { zs = zs.add(b1, (d-a)/(b-a), 1.0, anglea, angleb, true, true) } else if a < c-ppath.Epsilon { zs = zs.add(b0, (c-a)/(b-a), 0.0, anglea, angleb, true, true) } } else if inInterval(b, c, d) { // b in c-d same := c < b-ppath.Epsilon || d < b-ppath.Epsilon if c < b-ppath.Epsilon { zs = zs.add(b0, (c-a)/(b-a), 0.0, anglea, angleb, true, true) } else if d < b-ppath.Epsilon { zs = zs.add(b1, (d-a)/(b-a), 1.0, anglea, angleb, true, true) } zs = zs.add(a1, 1.0, (b-c)/(d-c), anglea, angleb, true, same) } } return zs } else if ppath.EqualPoint(a1, b0) { // handle common cases with endpoints to avoid numerical issues zs = zs.add(a1, 1.0, 0.0, anglea, angleb, true, false) return zs } else if ppath.EqualPoint(a0, b1) { // handle common cases with endpoints to avoid numerical issues zs = zs.add(a0, 0.0, 1.0, anglea, angleb, true, false) return zs } ta := db.Cross(a0.Sub(b0)) / div tb := da.Cross(a0.Sub(b0)) / div if inInterval(ta, 0.0, 1.0) && inInterval(tb, 0.0, 1.0) { tangent := ppath.Equal(ta, 0.0) || ppath.Equal(ta, 1.0) || ppath.Equal(tb, 0.0) || ppath.Equal(tb, 1.0) zs = zs.add(a0.Lerp(a1, ta), ta, tb, anglea, angleb, tangent, false) } return zs } // https://www.particleincell.com/2013/cubic-line-intersection/ func intersectionLineQuad(zs Intersections, l0, l1, p0, p1, p2 math32.Vector2) Intersections { if ppath.EqualPoint(l0, l1) { return zs // zero-length Close } // write line as A.X = bias A := math32.Vector2{l1.Y - l0.Y, l0.X - l1.X} bias := l0.Dot(A) a := A.Dot(p0.Sub(p1.MulScalar(2.0)).Add(p2)) b := A.Dot(p1.Sub(p0).MulScalar(2.0)) c := A.Dot(p0) - bias roots := []float32{} r0, r1 := solveQuadraticFormula(a, b, c) if !math32.IsNaN(r0) { roots = append(roots, r0) if !math32.IsNaN(r1) { roots = append(roots, r1) } } dira := ppath.Angle(l1.Sub(l0)) horizontal := math32.Abs(l1.Y-l0.Y) <= math32.Abs(l1.X-l0.X) for _, root := range roots { if inInterval(root, 0.0, 1.0) { var s float32 pos := quadraticBezierPos(p0, p1, p2, root) if horizontal { s = (pos.X - l0.X) / (l1.X - l0.X) } else { s = (pos.Y - l0.Y) / (l1.Y - l0.Y) } if inInterval(s, 0.0, 1.0) { deriv := ppath.QuadraticBezierDeriv(p0, p1, p2, root) dirb := ppath.Angle(deriv) endpoint := ppath.Equal(root, 0.0) || ppath.Equal(root, 1.0) || ppath.Equal(s, 0.0) || ppath.Equal(s, 1.0) if endpoint { // deviate angle slightly at endpoint when aligned to properly set Into deriv2 := quadraticBezierDeriv2(p0, p1, p2) if (0.0 <= deriv.Cross(deriv2)) == (ppath.Equal(root, 0.0) || !ppath.Equal(root, 1.0) && ppath.Equal(s, 0.0)) { dirb += ppath.Epsilon * 2.0 // t=0 and CCW, or t=1 and CW } else { dirb -= ppath.Epsilon * 2.0 // t=0 and CW, or t=1 and CCW } dirb = ppath.AngleNorm(dirb) } zs = zs.add(pos, s, root, dira, dirb, endpoint || ppath.Equal(A.Dot(deriv), 0.0), false) } } } return zs } // https://www.particleincell.com/2013/cubic-line-intersection/ func intersectionLineCube(zs Intersections, l0, l1, p0, p1, p2, p3 math32.Vector2) Intersections { if ppath.EqualPoint(l0, l1) { return zs // zero-length Close } // write line as A.X = bias A := math32.Vector2{l1.Y - l0.Y, l0.X - l1.X} bias := l0.Dot(A) a := A.Dot(p3.Sub(p0).Add(p1.MulScalar(3.0)).Sub(p2.MulScalar(3.0))) b := A.Dot(p0.MulScalar(3.0).Sub(p1.MulScalar(6.0)).Add(p2.MulScalar(3.0))) c := A.Dot(p1.MulScalar(3.0).Sub(p0.MulScalar(3.0))) d := A.Dot(p0) - bias roots := []float32{} r0, r1, r2 := solveCubicFormula(a, b, c, d) if !math32.IsNaN(r0) { roots = append(roots, r0) if !math32.IsNaN(r1) { roots = append(roots, r1) if !math32.IsNaN(r2) { roots = append(roots, r2) } } } dira := ppath.Angle(l1.Sub(l0)) horizontal := math32.Abs(l1.Y-l0.Y) <= math32.Abs(l1.X-l0.X) for _, root := range roots { if inInterval(root, 0.0, 1.0) { var s float32 pos := cubicBezierPos(p0, p1, p2, p3, root) if horizontal { s = (pos.X - l0.X) / (l1.X - l0.X) } else { s = (pos.Y - l0.Y) / (l1.Y - l0.Y) } if inInterval(s, 0.0, 1.0) { deriv := ppath.CubicBezierDeriv(p0, p1, p2, p3, root) dirb := ppath.Angle(deriv) tangent := ppath.Equal(A.Dot(deriv), 0.0) endpoint := ppath.Equal(root, 0.0) || ppath.Equal(root, 1.0) || ppath.Equal(s, 0.0) || ppath.Equal(s, 1.0) if endpoint { // deviate angle slightly at endpoint when aligned to properly set Into deriv2 := cubicBezierDeriv2(p0, p1, p2, p3, root) if (0.0 <= deriv.Cross(deriv2)) == (ppath.Equal(root, 0.0) || !ppath.Equal(root, 1.0) && ppath.Equal(s, 0.0)) { dirb += ppath.Epsilon * 2.0 // t=0 and CCW, or t=1 and CW } else { dirb -= ppath.Epsilon * 2.0 // t=0 and CW, or t=1 and CCW } } else if ppath.AngleEqual(dira, dirb) || ppath.AngleEqual(dira, dirb+math32.Pi) { // directions are parallel but the paths do cross (inflection point) // TODO: test better deriv2 := cubicBezierDeriv2(p0, p1, p2, p3, root) if ppath.Equal(deriv2.X, 0.0) && ppath.Equal(deriv2.Y, 0.0) { deriv3 := cubicBezierDeriv3(p0, p1, p2, p3, root) if 0.0 < deriv.Cross(deriv3) { dirb += ppath.Epsilon * 2.0 } else { dirb -= ppath.Epsilon * 2.0 } dirb = ppath.AngleNorm(dirb) tangent = false } } zs = zs.add(pos, s, root, dira, dirb, endpoint || tangent, false) } } } return zs } // handle line-arc intersections and their peculiarities regarding angles func addLineArcIntersection(zs Intersections, pos math32.Vector2, dira, dirb, t, t0, t1, angle, theta0, theta1 float32, tangent bool) Intersections { if theta0 <= theta1 { angle = theta0 - ppath.Epsilon + ppath.AngleNorm(angle-theta0+ppath.Epsilon) } else { angle = theta1 - ppath.Epsilon + ppath.AngleNorm(angle-theta1+ppath.Epsilon) } endpoint := ppath.Equal(t, t0) || ppath.Equal(t, t1) || ppath.Equal(angle, theta0) || ppath.Equal(angle, theta1) if endpoint { // deviate angle slightly at endpoint when aligned to properly set Into if (theta0 <= theta1) == (ppath.Equal(angle, theta0) || !ppath.Equal(angle, theta1) && ppath.Equal(t, t0)) { dirb += ppath.Epsilon * 2.0 // t=0 and CCW, or t=1 and CW } else { dirb -= ppath.Epsilon * 2.0 // t=0 and CW, or t=1 and CCW } dirb = ppath.AngleNorm(dirb) } // snap segment parameters to 0.0 and 1.0 to avoid numerical issues var s float32 if ppath.Equal(t, t0) { t = 0.0 } else if ppath.Equal(t, t1) { t = 1.0 } else { t = (t - t0) / (t1 - t0) } if ppath.Equal(angle, theta0) { s = 0.0 } else if ppath.Equal(angle, theta1) { s = 1.0 } else { s = (angle - theta0) / (theta1 - theta0) } return zs.add(pos, t, s, dira, dirb, endpoint || tangent, false) } // https://www.geometrictools.com/GTE/Mathematics/IntrLine2Circle2.h func intersectionLineCircle(zs Intersections, l0, l1, center math32.Vector2, radius, theta0, theta1 float32) Intersections { if ppath.EqualPoint(l0, l1) { return zs // zero-length Close } // solve l0 + t*(l1-l0) = P + t*D = X (line equation) // and |X - center| = |X - C| = R = radius (circle equation) // by substitution and squaring: |P + t*D - C|^2 = R^2 // giving: D^2 t^2 + 2D(P-C) t + (P-C)^2-R^2 = 0 dir := l1.Sub(l0) diff := l0.Sub(center) // P-C length := dir.Length() D := dir.DivScalar(length) // we normalise D to be of length 1, so that the roots are in [0,length] a := float32(1.0) b := 2.0 * D.Dot(diff) c := diff.Dot(diff) - radius*radius // find solutions for t ∈ [0,1], the parameter along the line's path roots := []float32{} r0, r1 := solveQuadraticFormula(a, b, c) if !math32.IsNaN(r0) { roots = append(roots, r0) if !math32.IsNaN(r1) && !ppath.Equal(r0, r1) { roots = append(roots, r1) } } // handle common cases with endpoints to avoid numerical issues // snap closest root to path's start or end if 0 < len(roots) { if pos := l0.Sub(center); ppath.Equal(pos.Length(), radius) { if len(roots) == 1 || math32.Abs(roots[0]) < math32.Abs(roots[1]) { roots[0] = 0.0 } else { roots[1] = 0.0 } } if pos := l1.Sub(center); ppath.Equal(pos.Length(), radius) { if len(roots) == 1 || math32.Abs(roots[0]-length) < math32.Abs(roots[1]-length) { roots[0] = length } else { roots[1] = length } } } // add intersections dira := ppath.Angle(dir) tangent := len(roots) == 1 for _, root := range roots { pos := diff.Add(dir.MulScalar(root / length)) angle := math32.Atan2(pos.Y*radius, pos.X*radius) if inInterval(root, 0.0, length) && ppath.IsAngleBetween(angle, theta0, theta1) { pos = center.Add(pos) dirb := ppath.Angle(ppath.EllipseDeriv(radius, radius, 0.0, theta0 <= theta1, angle)) zs = addLineArcIntersection(zs, pos, dira, dirb, root, 0.0, length, angle, theta0, theta1, tangent) } } return zs } func intersectionLineEllipse(zs Intersections, l0, l1, center, radius math32.Vector2, phi, theta0, theta1 float32) Intersections { if ppath.Equal(radius.X, radius.Y) { return intersectionLineCircle(zs, l0, l1, center, radius.X, theta0, theta1) } else if ppath.EqualPoint(l0, l1) { return zs // zero-length Close } // TODO: needs more testing // TODO: intersection inconsistency due to numerical stability in finding tangent collisions for subsequent paht segments (line -> ellipse), or due to the endpoint of a line not touching with another arc, but the subsequent segment does touch with its starting point dira := ppath.Angle(l1.Sub(l0)) // we take the ellipse center as the origin and counter-rotate by phi l0 = l0.Sub(center).Rot(-phi, ppath.Origin) l1 = l1.Sub(center).Rot(-phi, ppath.Origin) // line: cx + dy + e = 0 c := l0.Y - l1.Y d := l1.X - l0.X e := l0.Cross(l1) // follow different code paths when line is mostly horizontal or vertical horizontal := math32.Abs(c) <= math32.Abs(d) // ellipse: x^2/a + y^2/b = 1 a := radius.X * radius.X b := radius.Y * radius.Y // rewrite as a polynomial by substituting x or y to obtain: // At^2 + Bt + C = 0, with t either x (horizontal) or y (!horizontal) var A, B, C float32 A = a*c*c + b*d*d if horizontal { B = 2.0 * a * c * e C = a*e*e - a*b*d*d } else { B = 2.0 * b * d * e C = b*e*e - a*b*c*c } // find solutions roots := []float32{} r0, r1 := solveQuadraticFormula(A, B, C) if !math32.IsNaN(r0) { roots = append(roots, r0) if !math32.IsNaN(r1) && !ppath.Equal(r0, r1) { roots = append(roots, r1) } } for _, root := range roots { // get intersection position with center as origin var x, y, t0, t1 float32 if horizontal { x = root y = -e/d - c*root/d t0 = l0.X t1 = l1.X } else { x = -e/c - d*root/c y = root t0 = l0.Y t1 = l1.Y } tangent := ppath.Equal(root, 0.0) angle := math32.Atan2(y*radius.X, x*radius.Y) if inInterval(root, t0, t1) && ppath.IsAngleBetween(angle, theta0, theta1) { pos := math32.Vector2{x, y}.Rot(phi, ppath.Origin).Add(center) dirb := ppath.Angle(ppath.EllipseDeriv(radius.X, radius.Y, phi, theta0 <= theta1, angle)) zs = addLineArcIntersection(zs, pos, dira, dirb, root, t0, t1, angle, theta0, theta1, tangent) } } return zs } func intersectionEllipseEllipse(zs Intersections, c0, r0 math32.Vector2, phi0, thetaStart0, thetaEnd0 float32, c1, r1 math32.Vector2, phi1, thetaStart1, thetaEnd1 float32) Intersections { // TODO: needs more testing if !ppath.Equal(r0.X, r0.Y) || !ppath.Equal(r1.X, r1.Y) { panic("not handled") // ellipses } arcAngle := func(theta float32, sweep bool) float32 { theta += math32.Pi / 2.0 if !sweep { theta -= math32.Pi } return ppath.AngleNorm(theta) } dtheta0 := thetaEnd0 - thetaStart0 thetaStart0 = ppath.AngleNorm(thetaStart0 + phi0) thetaEnd0 = thetaStart0 + dtheta0 dtheta1 := thetaEnd1 - thetaStart1 thetaStart1 = ppath.AngleNorm(thetaStart1 + phi1) thetaEnd1 = thetaStart1 + dtheta1 if ppath.EqualPoint(c0, c1) && ppath.EqualPoint(r0, r1) { // parallel tOffset1 := float32(0.0) dirOffset1 := float32(0.0) if (0.0 <= dtheta0) != (0.0 <= dtheta1) { thetaStart1, thetaEnd1 = thetaEnd1, thetaStart1 // keep order on first arc dirOffset1 = math32.Pi tOffset1 = 1.0 } // will add either 1 (when touching) or 2 (when overlapping) intersections if t := angleTime(thetaStart0, thetaStart1, thetaEnd1); inInterval(t, 0.0, 1.0) { // ellipse0 starts within/on border of ellipse1 dir := arcAngle(thetaStart0, 0.0 <= dtheta0) pos := ppath.EllipsePos(r0.X, r0.Y, 0.0, c0.X, c0.Y, thetaStart0) zs = zs.add(pos, 0.0, math32.Abs(t-tOffset1), dir, ppath.AngleNorm(dir+dirOffset1), true, true) } if t := angleTime(thetaStart1, thetaStart0, thetaEnd0); inIntervalExclusive(t, 0.0, 1.0) { // ellipse1 starts within ellipse0 dir := arcAngle(thetaStart1, 0.0 <= dtheta0) pos := ppath.EllipsePos(r0.X, r0.Y, 0.0, c0.X, c0.Y, thetaStart1) zs = zs.add(pos, t, tOffset1, dir, ppath.AngleNorm(dir+dirOffset1), true, true) } if t := angleTime(thetaEnd1, thetaStart0, thetaEnd0); inIntervalExclusive(t, 0.0, 1.0) { // ellipse1 ends within ellipse0 dir := arcAngle(thetaEnd1, 0.0 <= dtheta0) pos := ppath.EllipsePos(r0.X, r0.Y, 0.0, c0.X, c0.Y, thetaEnd1) zs = zs.add(pos, t, 1.0-tOffset1, dir, ppath.AngleNorm(dir+dirOffset1), true, true) } if t := angleTime(thetaEnd0, thetaStart1, thetaEnd1); inInterval(t, 0.0, 1.0) { // ellipse0 ends within/on border of ellipse1 dir := arcAngle(thetaEnd0, 0.0 <= dtheta0) pos := ppath.EllipsePos(r0.X, r0.Y, 0.0, c0.X, c0.Y, thetaEnd0) zs = zs.add(pos, 1.0, math32.Abs(t-tOffset1), dir, ppath.AngleNorm(dir+dirOffset1), true, true) } return zs } // https://math32.stackexchange.com/questions/256100/how-can-i-find-the-points-at-which-two-circles-intersect // https://gist.github.com/jupdike/bfe5eb23d1c395d8a0a1a4ddd94882ac R := c0.Sub(c1).Length() if R < math32.Abs(r0.X-r1.X) || r0.X+r1.X < R { return zs } R2 := R * R k := r0.X*r0.X - r1.X*r1.X a := float32(0.5) b := 0.5 * k / R2 c := 0.5 * math32.Sqrt(2.0*(r0.X*r0.X+r1.X*r1.X)/R2-k*k/(R2*R2)-1.0) mid := c1.Sub(c0).MulScalar(a + b) dev := math32.Vector2{c1.Y - c0.Y, c0.X - c1.X}.MulScalar(c) tangent := ppath.EqualPoint(dev, math32.Vector2{}) anglea0 := ppath.Angle(mid.Add(dev)) anglea1 := ppath.Angle(c0.Sub(c1).Add(mid).Add(dev)) ta0 := angleTime(anglea0, thetaStart0, thetaEnd0) ta1 := angleTime(anglea1, thetaStart1, thetaEnd1) if inInterval(ta0, 0.0, 1.0) && inInterval(ta1, 0.0, 1.0) { dir0 := arcAngle(anglea0, 0.0 <= dtheta0) dir1 := arcAngle(anglea1, 0.0 <= dtheta1) endpoint := ppath.Equal(ta0, 0.0) || ppath.Equal(ta0, 1.0) || ppath.Equal(ta1, 0.0) || ppath.Equal(ta1, 1.0) zs = zs.add(c0.Add(mid).Add(dev), ta0, ta1, dir0, dir1, tangent || endpoint, false) } if !tangent { angleb0 := ppath.Angle(mid.Sub(dev)) angleb1 := ppath.Angle(c0.Sub(c1).Add(mid).Sub(dev)) tb0 := angleTime(angleb0, thetaStart0, thetaEnd0) tb1 := angleTime(angleb1, thetaStart1, thetaEnd1) if inInterval(tb0, 0.0, 1.0) && inInterval(tb1, 0.0, 1.0) { dir0 := arcAngle(angleb0, 0.0 <= dtheta0) dir1 := arcAngle(angleb1, 0.0 <= dtheta1) endpoint := ppath.Equal(tb0, 0.0) || ppath.Equal(tb0, 1.0) || ppath.Equal(tb1, 0.0) || ppath.Equal(tb1, 1.0) zs = zs.add(c0.Add(mid).Sub(dev), tb0, tb1, dir0, dir1, endpoint, false) } } return zs } // TODO: bezier-bezier intersection // TODO: bezier-ellipse intersection // For Bézier-Bézier intersections: // see T.W. Sederberg, "Computer Aided Geometric Design", 2012 // see T.W. Sederberg and T. Nishita, "Curve intersection using Bézier clipping", 1990 // see T.W. Sederberg and S.R. Parry, "Comparison of three curve intersection algorithms", 1986 func IntersectionRayLine(a0, a1, b0, b1 math32.Vector2) (math32.Vector2, bool) { da := a1.Sub(a0) db := b1.Sub(b0) div := da.Cross(db) if ppath.Equal(div, 0.0) { // parallel return math32.Vector2{}, false } tb := da.Cross(a0.Sub(b0)) / div if inInterval(tb, 0.0, 1.0) { return b0.Lerp(b1, tb), true } return math32.Vector2{}, false } // https://mathworld.wolfram.com/Circle-LineIntersection.html func IntersectionRayCircle(l0, l1, c math32.Vector2, r float32) (math32.Vector2, math32.Vector2, bool) { d := l1.Sub(l0).Normal() // along line direction, anchored in l0, its length is 1 D := l0.Sub(c).Cross(d) discriminant := r*r - D*D if discriminant < 0 { return math32.Vector2{}, math32.Vector2{}, false } discriminant = math32.Sqrt(discriminant) ax := D * d.Y bx := d.X * discriminant if d.Y < 0.0 { bx = -bx } ay := -D * d.X by := math32.Abs(d.Y) * discriminant return c.Add(math32.Vector2{ax + bx, ay + by}), c.Add(math32.Vector2{ax - bx, ay - by}), true } // https://math32.stackexchange.com/questions/256100/how-can-i-find-the-points-at-which-two-circles-intersect // https://gist.github.com/jupdike/bfe5eb23d1c395d8a0a1a4ddd94882ac func IntersectionCircleCircle(c0 math32.Vector2, r0 float32, c1 math32.Vector2, r1 float32) (math32.Vector2, math32.Vector2, bool) { R := c0.Sub(c1).Length() if R < math32.Abs(r0-r1) || r0+r1 < R || ppath.EqualPoint(c0, c1) { return math32.Vector2{}, math32.Vector2{}, false } R2 := R * R k := r0*r0 - r1*r1 a := float32(0.5) b := 0.5 * k / R2 c := 0.5 * math32.Sqrt(2.0*(r0*r0+r1*r1)/R2-k*k/(R2*R2)-1.0) i0 := c0.Add(c1).MulScalar(a) i1 := c1.Sub(c0).MulScalar(b) i2 := math32.Vector2{c1.Y - c0.Y, c0.X - c1.X}.MulScalar(c) return i0.Add(i1).Add(i2), i0.Add(i1).Sub(i2), true } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package intersect import ( "fmt" "strings" "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" ) // inInterval returns true if f is in closed interval // [lower-Epsilon,upper+Epsilon] where lower and upper can be interchanged. func inInterval(f, lower, upper float32) bool { if upper < lower { lower, upper = upper, lower } return lower-ppath.Epsilon <= f && f <= upper+ppath.Epsilon } // inIntervalExclusive returns true if f is in open interval // [lower+Epsilon,upper-Epsilon] where lower and upper can be interchanged. func inIntervalExclusive(f, lower, upper float32) bool { if upper < lower { lower, upper = upper, lower } return lower+ppath.Epsilon < f && f < upper-ppath.Epsilon } // touchesPoint returns true if the rectangle touches a point (within +-Epsilon). func touchesPoint(r math32.Box2, p math32.Vector2) bool { return inInterval(p.X, r.Min.X, r.Max.X) && inInterval(p.Y, r.Min.Y, r.Max.Y) } // touches returns true if both rectangles touch (or overlap). func touches(r, q math32.Box2) bool { if q.Max.X+ppath.Epsilon < r.Min.X || r.Max.X < q.Min.X-ppath.Epsilon { // left or right return false } else if q.Max.Y+ppath.Epsilon < r.Min.Y || r.Max.Y < q.Min.Y-ppath.Epsilon { // below or above return false } return true } // angleBetweenExclusive is true when theta is in range (lower,upper) // excluding the end points. Angles can be outside the [0,2PI) range. func angleBetweenExclusive(theta, lower, upper float32) bool { if upper < lower { // sweep is false, ie direction is along negative angle (clockwise) lower, upper = upper, lower } theta = ppath.AngleNorm(theta - lower) upper = ppath.AngleNorm(upper - lower) if 0.0 < theta && theta < upper { return true } return false } // angleTime returns the time [0.0,1.0] of theta between // [lower,upper]. When outside of [lower,upper], the result will also be outside of [0.0,1.0]. func angleTime(theta, lower, upper float32) float32 { sweep := true if upper < lower { // sweep is false, ie direction is along negative angle (clockwise) lower, upper = upper, lower sweep = false } theta = ppath.AngleNorm(theta - lower + ppath.Epsilon) upper = ppath.AngleNorm(upper - lower) t := (theta - ppath.Epsilon) / upper if !sweep { t = 1.0 - t } if ppath.Equal(t, 0.0) { return 0.0 } else if ppath.Equal(t, 1.0) { return 1.0 } return t } // Numerically stable quadratic formula, lowest root is returned first, see https://math32.stackexchange.com/a/2007723 func solveQuadraticFormula(a, b, c float32) (float32, float32) { if ppath.Equal(a, 0.0) { if ppath.Equal(b, 0.0) { if ppath.Equal(c, 0.0) { // all terms disappear, all x satisfy the solution return 0.0, math32.NaN() } // linear term disappears, no solutions return math32.NaN(), math32.NaN() } // quadratic term disappears, solve linear equation return -c / b, math32.NaN() } if ppath.Equal(c, 0.0) { // no constant term, one solution at zero and one from solving linearly if ppath.Equal(b, 0.0) { return 0.0, math32.NaN() } return 0.0, -b / a } discriminant := b*b - 4.0*a*c if discriminant < 0.0 { return math32.NaN(), math32.NaN() } else if ppath.Equal(discriminant, 0.0) { return -b / (2.0 * a), math32.NaN() } // Avoid catastrophic cancellation, which occurs when we subtract two nearly equal numbers and causes a large error. This can be the case when 4*a*c is small so that sqrt(discriminant) -> b, and the sign of b and in front of the radical are the same. Instead, we calculate x where b and the radical have different signs, and then use this result in the analytical equivalent of the formula, called the Citardauq Formula. q := math32.Sqrt(discriminant) if b < 0.0 { // apply sign of b q = -q } x1 := -(b + q) / (2.0 * a) x2 := c / (a * x1) if x2 < x1 { x1, x2 = x2, x1 } return x1, x2 } // see https://www.geometrictools.com/Documentation/LowDegreePolynomialRoots.pdf // see https://github.com/thelonious/kld-polynomial/blob/development/lib/Polynomial.js func solveCubicFormula(a, b, c, d float32) (float32, float32, float32) { var x1, x2, x3 float32 x2, x3 = math32.NaN(), math32.NaN() // x1 is always set to a number below if ppath.Equal(a, 0.0) { x1, x2 = solveQuadraticFormula(b, c, d) } else { // obtain monic polynomial: x^3 + f.x^2 + g.x + h = 0 b /= a c /= a d /= a // obtain depressed polynomial: x^3 + c1.x + c0 bthird := b / 3.0 c0 := d - bthird*(c-2.0*bthird*bthird) c1 := c - b*bthird if ppath.Equal(c0, 0.0) { if c1 < 0.0 { tmp := math32.Sqrt(-c1) x1 = -tmp - bthird x2 = tmp - bthird x3 = 0.0 - bthird } else { x1 = 0.0 - bthird } } else if ppath.Equal(c1, 0.0) { if 0.0 < c0 { x1 = -math32.Cbrt(c0) - bthird } else { x1 = math32.Cbrt(-c0) - bthird } } else { delta := -(4.0*c1*c1*c1 + 27.0*c0*c0) if ppath.Equal(delta, 0.0) { delta = 0.0 } if delta < 0.0 { betaRe := -c0 / 2.0 betaIm := math32.Sqrt(-delta / 108.0) tmp := betaRe - betaIm if 0.0 <= tmp { x1 = math32.Cbrt(tmp) } else { x1 = -math32.Cbrt(-tmp) } tmp = betaRe + betaIm if 0.0 <= tmp { x1 += math32.Cbrt(tmp) } else { x1 -= math32.Cbrt(-tmp) } x1 -= bthird } else if 0.0 < delta { betaRe := -c0 / 2.0 betaIm := math32.Sqrt(delta / 108.0) theta := math32.Atan2(betaIm, betaRe) / 3.0 sintheta, costheta := math32.Sincos(theta) distance := math32.Sqrt(-c1 / 3.0) // same as rhoPowThird tmp := distance * sintheta * math32.Sqrt(3.0) x1 = 2.0*distance*costheta - bthird x2 = -distance*costheta - tmp - bthird x3 = -distance*costheta + tmp - bthird } else { tmp := -3.0 * c0 / (2.0 * c1) x1 = tmp - bthird x2 = -2.0*tmp - bthird } } } // sort if x3 < x2 || math32.IsNaN(x2) { x2, x3 = x3, x2 } if x2 < x1 || math32.IsNaN(x1) { x1, x2 = x2, x1 } if x3 < x2 || math32.IsNaN(x2) { x2, x3 = x3, x2 } return x1, x2, x3 } type gaussLegendreFunc func(func(float32) float32, float32, float32) float32 // Gauss-Legendre quadrature integration from a to b with n=3, see https://pomax.github.io/bezierinfo/legendre-gauss.html for more values func gaussLegendre3(f func(float32) float32, a, b float32) float32 { c := (b - a) / 2.0 d := (a + b) / 2.0 Qd1 := f(-0.774596669*c + d) Qd2 := f(d) Qd3 := f(0.774596669*c + d) return c * ((5.0/9.0)*(Qd1+Qd3) + (8.0/9.0)*Qd2) } // Gauss-Legendre quadrature integration from a to b with n=5 func gaussLegendre5(f func(float32) float32, a, b float32) float32 { c := (b - a) / 2.0 d := (a + b) / 2.0 Qd1 := f(-0.90618*c + d) Qd2 := f(-0.538469*c + d) Qd3 := f(d) Qd4 := f(0.538469*c + d) Qd5 := f(0.90618*c + d) return c * (0.236927*(Qd1+Qd5) + 0.478629*(Qd2+Qd4) + 0.568889*Qd3) } // Gauss-Legendre quadrature integration from a to b with n=7 func gaussLegendre7(f func(float32) float32, a, b float32) float32 { c := (b - a) / 2.0 d := (a + b) / 2.0 Qd1 := f(-0.949108*c + d) Qd2 := f(-0.741531*c + d) Qd3 := f(-0.405845*c + d) Qd4 := f(d) Qd5 := f(0.405845*c + d) Qd6 := f(0.741531*c + d) Qd7 := f(0.949108*c + d) return c * (0.129485*(Qd1+Qd7) + 0.279705*(Qd2+Qd6) + 0.381830*(Qd3+Qd5) + 0.417959*Qd4) } func invSpeedPolynomialChebyshevApprox(N int, gaussLegendre gaussLegendreFunc, fp func(float32) float32, tmin, tmax float32) (func(float32) float32, float32) { // TODO: find better way to determine N. For Arc 10 seems fine, for some Quads 10 is too low, for Cube depending on inflection points is maybe not the best indicator // TODO: track efficiency, how many times is fp called? Does a look-up table make more sense? fLength := func(t float32) float32 { return math32.Abs(gaussLegendre(fp, tmin, t)) } totalLength := fLength(tmax) t := func(L float32) float32 { return bisectionMethod(fLength, L, tmin, tmax) } return polynomialChebyshevApprox(N, t, 0.0, totalLength, tmin, tmax), totalLength } func polynomialChebyshevApprox(N int, f func(float32) float32, xmin, xmax, ymin, ymax float32) func(float32) float32 { fs := make([]float32, N) for k := 0; k < N; k++ { u := math32.Cos(math32.Pi * (float32(k+1) - 0.5) / float32(N)) fs[k] = f(xmin + (xmax-xmin)*(u+1.0)/2.0) } c := make([]float32, N) for j := 0; j < N; j++ { a := float32(0.0) for k := 0; k < N; k++ { a += fs[k] * math32.Cos(float32(j)*math32.Pi*(float32(k+1)-0.5)/float32(N)) } c[j] = (2.0 / float32(N)) * a } if ymax < ymin { ymin, ymax = ymax, ymin } return func(x float32) float32 { x = math32.Min(xmax, math32.Max(xmin, x)) u := (x-xmin)/(xmax-xmin)*2.0 - 1.0 a := float32(0.0) for j := 0; j < N; j++ { a += c[j] * math32.Cos(float32(j)*math32.Acos(u)) } y := -0.5*c[0] + a if !math32.IsNaN(ymin) && !math32.IsNaN(ymax) { y = math32.Min(ymax, math32.Max(ymin, y)) } return y } } // find value x for which f(x) = y in the interval x in [xmin, xmax] using the bisection method func bisectionMethod(f func(float32) float32, y, xmin, xmax float32) float32 { const MaxIterations = 100 const Tolerance = 0.001 // 0.1% n := 0 toleranceX := math32.Abs(xmax-xmin) * Tolerance toleranceY := math32.Abs(f(xmax)-f(xmin)) * Tolerance var x float32 for { x = (xmin + xmax) / 2.0 if n >= MaxIterations { return x } dy := f(x) - y if math32.Abs(dy) < toleranceY || math32.Abs(xmax-xmin)/2.0 < toleranceX { return x } else if dy > 0.0 { xmax = x } else { xmin = x } n++ } } // snap "gridsnaps" the floating point to a grid of the given spacing func snap(val, spacing float32) float32 { return math32.Round(val/spacing) * spacing } // Gridsnap snaps point to a grid with the given spacing. func Gridsnap(p math32.Vector2, spacing float32) math32.Vector2 { return math32.Vector2{snap(p.X, spacing), snap(p.Y, spacing)} } type numEps float32 func (f numEps) String() string { s := fmt.Sprintf("%.*g", int(math32.Ceil(-math32.Log10(ppath.Epsilon))), f) if dot := strings.IndexByte(s, '.'); dot != -1 { for dot < len(s) && s[len(s)-1] == '0' { s = s[:len(s)-1] } if dot < len(s) && s[len(s)-1] == '.' { s = s[:len(s)-1] } } return s } //func lookupMin(f func(float64) float64, xmin, xmax float64) float64 { // const MaxIterations = 1000 // min := math32.Inf(1) // for i := 0; i <= MaxIterations; i++ { // t := float64(i) / float64(MaxIterations) // x := xmin + t*(xmax-xmin) // y := f(x) // if y < min { // min = y // } // } // return min //} // //func gradientDescent(f func(float64) float64, xmin, xmax float64) float64 { // const MaxIterations = 100 // const Delta = 0.0001 // const Rate = 0.01 // // x := (xmin + xmax) / 2.0 // for i := 0; i < MaxIterations; i++ { // dydx := (f(x+Delta) - f(x-Delta)) / 2.0 / Delta // x -= Rate * dydx // } // return x //} // func cohenSutherlandOutcode(rect math32.Box2, p math32.Vector2, eps float32) int { // code := 0b0000 // if p.X < rect.Min.X-eps { // code |= 0b0001 // left // } else if rect.Max.X+eps < p.X { // code |= 0b0010 // right // } // if p.Y < rect.Min.Y-eps { // code |= 0b0100 // bottom // } else if rect.Max.Y+eps < p.Y { // code |= 0b1000 // top // } // return code // } // // // return whether line is inside the rectangle, either entirely or partially. // func cohenSutherlandLineClip(rect math32.Box2, a, b math32.Vector2, eps float32) (math32.Vector2, math32.Vector2, bool, bool) { // outcode0 := cohenSutherlandOutcode(rect, a, eps) // outcode1 := cohenSutherlandOutcode(rect, b, eps) // if outcode0 == 0 && outcode1 == 0 { // return a, b, true, false // } // for { // if (outcode0 | outcode1) == 0 { // // both inside // return a, b, true, true // } else if (outcode0 & outcode1) != 0 { // // both in same region outside // return a, b, false, false // } // // // pick point outside // outcodeOut := outcode0 // if outcode0 < outcode1 { // outcodeOut = outcode1 // } // // // intersect with rectangle // var c math32.Vector2 // if (outcodeOut & 0b1000) != 0 { // // above // c.X = a.X + (b.X-a.X)*(rect.Max.Y-a.Y)/(b.Y-a.Y) // c.Y = rect.Max.Y // } else if (outcodeOut & 0b0100) != 0 { // // below // c.X = a.X + (b.X-a.X)*(rect.Min.Y-a.Y)/(b.Y-a.Y) // c.Y = rect.Min.Y // } else if (outcodeOut & 0b0010) != 0 { // // right // c.X = rect.Max.X // c.Y = a.Y + (b.Y-a.Y)*(rect.Max.X-a.X)/(b.X-a.X) // } else if (outcodeOut & 0b0001) != 0 { // // left // c.X = rect.Min.X // c.Y = a.Y + (b.Y-a.Y)*(rect.Min.X-a.X)/(b.X-a.X) // } // // // prepare next pass // if outcodeOut == outcode0 { // outcode0 = cohenSutherlandOutcode(rect, c, eps) // a = c // } else { // outcode1 = cohenSutherlandOutcode(rect, c, eps) // b = c // } // } // } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import ( "fmt" "math" "strings" "cogentcore.org/core/math32" "github.com/tdewolff/parse/v2/strconv" ) type num float32 func (f num) String() string { s := fmt.Sprintf("%.*g", Precision, f) if num(math.MaxInt32) < f || f < num(math.MinInt32) { if i := strings.IndexAny(s, ".eE"); i == -1 { s += ".0" } } return string(MinifyNumber([]byte(s), Precision)) } type dec float32 func (f dec) String() string { s := fmt.Sprintf("%.*f", Precision, f) s = string(MinifyDecimal([]byte(s), Precision)) if dec(math.MaxInt32) < f || f < dec(math.MinInt32) { if i := strings.IndexByte(s, '.'); i == -1 { s += ".0" } } return s } func skipCommaWhitespace(path []byte) int { i := 0 for i < len(path) && (path[i] == ' ' || path[i] == ',' || path[i] == '\n' || path[i] == '\r' || path[i] == '\t') { i++ } return i } // MustParseSVGPath parses an SVG path data string and panics if it fails. func MustParseSVGPath(s string) Path { p, err := ParseSVGPath(s) if err != nil { panic(err) } return p } // ParseSVGPath parses an SVG path data string. func ParseSVGPath(s string) (Path, error) { if len(s) == 0 { return Path{}, nil } i := 0 path := []byte(s) i += skipCommaWhitespace(path[i:]) if path[0] == ',' || path[i] < 'A' { return nil, fmt.Errorf("bad path: path should start with command") } cmdLens := map[byte]int{ 'M': 2, 'Z': 0, 'L': 2, 'H': 1, 'V': 1, 'C': 6, 'S': 4, 'Q': 4, 'T': 2, 'A': 7, } f := [7]float32{} p := Path{} var q, c math32.Vector2 var p0, p1 math32.Vector2 prevCmd := byte('z') for { i += skipCommaWhitespace(path[i:]) if len(path) <= i { break } cmd := prevCmd repeat := true if cmd == 'z' || cmd == 'Z' || !(path[i] >= '0' && path[i] <= '9' || path[i] == '.' || path[i] == '-' || path[i] == '+') { cmd = path[i] repeat = false i++ i += skipCommaWhitespace(path[i:]) } CMD := cmd if 'a' <= cmd && cmd <= 'z' { CMD -= 'a' - 'A' } for j := 0; j < cmdLens[CMD]; j++ { if CMD == 'A' && (j == 3 || j == 4) { // parse largeArc and sweep booleans for A command if i < len(path) && path[i] == '1' { f[j] = 1.0 } else if i < len(path) && path[i] == '0' { f[j] = 0.0 } else { return nil, fmt.Errorf("bad path: largeArc and sweep flags should be 0 or 1 in command '%c' at position %d", cmd, i+1) } i++ } else { num, n := strconv.ParseFloat(path[i:]) if n == 0 { if repeat && j == 0 && i < len(path) { return nil, fmt.Errorf("bad path: unknown command '%c' at position %d", path[i], i+1) } else if 1 < cmdLens[CMD] { return nil, fmt.Errorf("bad path: sets of %d numbers should follow command '%c' at position %d", cmdLens[CMD], cmd, i+1) } else { return nil, fmt.Errorf("bad path: number should follow command '%c' at position %d", cmd, i+1) } } f[j] = float32(num) i += n } i += skipCommaWhitespace(path[i:]) } switch cmd { case 'M', 'm': p1 = math32.Vector2{f[0], f[1]} if cmd == 'm' { p1 = p1.Add(p0) cmd = 'l' } else { cmd = 'L' } p.MoveTo(p1.X, p1.Y) case 'Z', 'z': p1 = p.StartPos() p.Close() case 'L', 'l': p1 = math32.Vector2{f[0], f[1]} if cmd == 'l' { p1 = p1.Add(p0) } p.LineTo(p1.X, p1.Y) case 'H', 'h': p1.X = f[0] if cmd == 'h' { p1.X += p0.X } p.LineTo(p1.X, p1.Y) case 'V', 'v': p1.Y = f[0] if cmd == 'v' { p1.Y += p0.Y } p.LineTo(p1.X, p1.Y) case 'C', 'c': cp1 := math32.Vector2{f[0], f[1]} cp2 := math32.Vector2{f[2], f[3]} p1 = math32.Vector2{f[4], f[5]} if cmd == 'c' { cp1 = cp1.Add(p0) cp2 = cp2.Add(p0) p1 = p1.Add(p0) } p.CubeTo(cp1.X, cp1.Y, cp2.X, cp2.Y, p1.X, p1.Y) c = cp2 case 'S', 's': cp1 := p0 cp2 := math32.Vector2{f[0], f[1]} p1 = math32.Vector2{f[2], f[3]} if cmd == 's' { cp2 = cp2.Add(p0) p1 = p1.Add(p0) } if prevCmd == 'C' || prevCmd == 'c' || prevCmd == 'S' || prevCmd == 's' { cp1 = p0.MulScalar(2.0).Sub(c) } p.CubeTo(cp1.X, cp1.Y, cp2.X, cp2.Y, p1.X, p1.Y) c = cp2 case 'Q', 'q': cp := math32.Vector2{f[0], f[1]} p1 = math32.Vector2{f[2], f[3]} if cmd == 'q' { cp = cp.Add(p0) p1 = p1.Add(p0) } p.QuadTo(cp.X, cp.Y, p1.X, p1.Y) q = cp case 'T', 't': cp := p0 p1 = math32.Vector2{f[0], f[1]} if cmd == 't' { p1 = p1.Add(p0) } if prevCmd == 'Q' || prevCmd == 'q' || prevCmd == 'T' || prevCmd == 't' { cp = p0.MulScalar(2.0).Sub(q) } p.QuadTo(cp.X, cp.Y, p1.X, p1.Y) q = cp case 'A', 'a': rx := f[0] ry := f[1] rot := f[2] large := f[3] == 1.0 sweep := f[4] == 1.0 p1 = math32.Vector2{f[5], f[6]} if cmd == 'a' { p1 = p1.Add(p0) } p.ArcToDeg(rx, ry, rot, large, sweep, p1.X, p1.Y) default: return nil, fmt.Errorf("bad path: unknown command '%c' at position %d", cmd, i+1) } prevCmd = cmd p0 = p1 } return p, nil } // String returns a string that represents the path similar to the SVG // path data format (but not necessarily valid SVG). func (p Path) String() string { sb := strings.Builder{} for i := 0; i < len(p); { cmd := p[i] switch cmd { case MoveTo: fmt.Fprintf(&sb, "M%g %g", p[i+1], p[i+2]) case LineTo: fmt.Fprintf(&sb, "L%g %g", p[i+1], p[i+2]) case QuadTo: fmt.Fprintf(&sb, "Q%g %g %g %g", p[i+1], p[i+2], p[i+3], p[i+4]) case CubeTo: fmt.Fprintf(&sb, "C%g %g %g %g %g %g", p[i+1], p[i+2], p[i+3], p[i+4], p[i+5], p[i+6]) case ArcTo: rot := math32.RadToDeg(p[i+3]) large, sweep := ToArcFlags(p[i+4]) sLarge := "0" if large { sLarge = "1" } sSweep := "0" if sweep { sSweep = "1" } fmt.Fprintf(&sb, "A%g %g %g %s %s %g %g", p[i+1], p[i+2], rot, sLarge, sSweep, p[i+5], p[i+6]) case Close: fmt.Fprintf(&sb, "z") } i += CmdLen(cmd) } return sb.String() } // ToSVG returns a string that represents the path in the SVG path data format with minification. func (p Path) ToSVG() string { if p.Empty() { return "" } sb := strings.Builder{} var x, y float32 for i := 0; i < len(p); { cmd := p[i] switch cmd { case MoveTo: x, y = p[i+1], p[i+2] fmt.Fprintf(&sb, "M%v %v", num(x), num(y)) case LineTo: xStart, yStart := x, y x, y = p[i+1], p[i+2] if Equal(x, xStart) && Equal(y, yStart) { // nothing } else if Equal(x, xStart) { fmt.Fprintf(&sb, "V%v", num(y)) } else if Equal(y, yStart) { fmt.Fprintf(&sb, "H%v", num(x)) } else { fmt.Fprintf(&sb, "L%v %v", num(x), num(y)) } case QuadTo: x, y = p[i+3], p[i+4] fmt.Fprintf(&sb, "Q%v %v %v %v", num(p[i+1]), num(p[i+2]), num(x), num(y)) case CubeTo: x, y = p[i+5], p[i+6] fmt.Fprintf(&sb, "C%v %v %v %v %v %v", num(p[i+1]), num(p[i+2]), num(p[i+3]), num(p[i+4]), num(x), num(y)) case ArcTo: rx, ry := p[i+1], p[i+2] rot := math32.RadToDeg(p[i+3]) large, sweep := ToArcFlags(p[i+4]) x, y = p[i+5], p[i+6] sLarge := "0" if large { sLarge = "1" } sSweep := "0" if sweep { sSweep = "1" } if 90.0 <= rot { rx, ry = ry, rx rot -= 90.0 } fmt.Fprintf(&sb, "A%v %v %v %s%s%v %v", num(rx), num(ry), num(rot), sLarge, sSweep, num(p[i+5]), num(p[i+6])) case Close: x, y = p[i+1], p[i+2] fmt.Fprintf(&sb, "z") } i += CmdLen(cmd) } return sb.String() } // ToPS returns a string that represents the path in the PostScript data format. func (p Path) ToPS() string { if p.Empty() { return "" } sb := strings.Builder{} var x, y float32 for i := 0; i < len(p); { cmd := p[i] switch cmd { case MoveTo: x, y = p[i+1], p[i+2] fmt.Fprintf(&sb, " %v %v moveto", dec(x), dec(y)) case LineTo: x, y = p[i+1], p[i+2] fmt.Fprintf(&sb, " %v %v lineto", dec(x), dec(y)) case QuadTo, CubeTo: var start, cp1, cp2 math32.Vector2 start = math32.Vector2{x, y} if cmd == QuadTo { x, y = p[i+3], p[i+4] cp1, cp2 = QuadraticToCubicBezier(start, math32.Vec2(p[i+1], p[i+2]), math32.Vector2{x, y}) } else { cp1 = math32.Vec2(p[i+1], p[i+2]) cp2 = math32.Vec2(p[i+3], p[i+4]) x, y = p[i+5], p[i+6] } fmt.Fprintf(&sb, " %v %v %v %v %v %v curveto", dec(cp1.X), dec(cp1.Y), dec(cp2.X), dec(cp2.Y), dec(x), dec(y)) case ArcTo: x0, y0 := x, y rx, ry, phi := p[i+1], p[i+2], p[i+3] large, sweep := ToArcFlags(p[i+4]) x, y = p[i+5], p[i+6] cx, cy, theta0, theta1 := EllipseToCenter(x0, y0, rx, ry, phi, large, sweep, x, y) theta0 = math32.RadToDeg(theta0) theta1 = math32.RadToDeg(theta1) rot := math32.RadToDeg(phi) fmt.Fprintf(&sb, " %v %v %v %v %v %v %v ellipse", dec(cx), dec(cy), dec(rx), dec(ry), dec(theta0), dec(theta1), dec(rot)) if !sweep { fmt.Fprintf(&sb, "n") } case Close: x, y = p[i+1], p[i+2] fmt.Fprintf(&sb, " closepath") } i += CmdLen(cmd) } return sb.String()[1:] // remove the first space } // ToPDF returns a string that represents the path in the PDF data format. func (p Path) ToPDF() string { if p.Empty() { return "" } p = p.ReplaceArcs() sb := strings.Builder{} var x, y float32 for i := 0; i < len(p); { cmd := p[i] switch cmd { case MoveTo: x, y = p[i+1], p[i+2] fmt.Fprintf(&sb, " %v %v m", dec(x), dec(y)) case LineTo: x, y = p[i+1], p[i+2] fmt.Fprintf(&sb, " %v %v l", dec(x), dec(y)) case QuadTo, CubeTo: var start, cp1, cp2 math32.Vector2 start = math32.Vector2{x, y} if cmd == QuadTo { x, y = p[i+3], p[i+4] cp1, cp2 = QuadraticToCubicBezier(start, math32.Vec2(p[i+1], p[i+2]), math32.Vector2{x, y}) } else { cp1 = math32.Vec2(p[i+1], p[i+2]) cp2 = math32.Vec2(p[i+3], p[i+4]) x, y = p[i+5], p[i+6] } fmt.Fprintf(&sb, " %v %v %v %v %v %v c", dec(cp1.X), dec(cp1.Y), dec(cp2.X), dec(cp2.Y), dec(x), dec(y)) case ArcTo: panic("arcs should have been replaced") case Close: x, y = p[i+1], p[i+2] fmt.Fprintf(&sb, " h") } i += CmdLen(cmd) } return sb.String()[1:] // remove the first space } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import ( "cogentcore.org/core/math32" ) var ( // Tolerance is the maximum deviation from the original path in millimeters // when e.g. flatting. Used for flattening in the renderers, font decorations, // and path intersections. Tolerance = float32(0.01) // PixelTolerance is the maximum deviation of the rasterized path from // the original for flattening purposed in pixels. PixelTolerance = float32(0.1) // In C, FLT_EPSILON = 1.19209e-07 // Epsilon is the smallest number below which we assume the value to be zero. // This is to avoid numerical floating point issues. Epsilon = float32(1e-7) // Precision is the number of significant digits at which floating point // value will be printed to output formats. Precision = 7 // Origin is the coordinate system's origin. Origin = math32.Vector2{0.0, 0.0} ) // Equal returns true if a and b are equal within an absolute // tolerance of Epsilon. func Equal(a, b float32) bool { // avoid math32.Abs if a < b { return b-a <= Epsilon } return a-b <= Epsilon } func EqualPoint(a, b math32.Vector2) bool { return Equal(a.X, b.X) && Equal(a.Y, b.Y) } // AngleEqual returns true if both angles are equal. func AngleEqual(a, b float32) bool { return IsAngleBetween(a, b, b) // IsAngleBetween will add Epsilon to lower and upper } // AngleNorm returns the angle theta in the range [0,2PI). func AngleNorm(theta float32) float32 { theta = math32.Mod(theta, 2.0*math32.Pi) if theta < 0.0 { theta += 2.0 * math32.Pi } return theta } // IsAngleBetween is true when theta is in range [lower,upper] // including the end points. Angles can be outside the [0,2PI) range. func IsAngleBetween(theta, lower, upper float32) bool { if upper < lower { // sweep is false, ie direction is along negative angle (clockwise) lower, upper = upper, lower } theta = AngleNorm(theta - lower + Epsilon) upper = AngleNorm(upper - lower + 2.0*Epsilon) return theta <= upper } // Slope returns the slope between OP, i.e. y/x. func Slope(p math32.Vector2) float32 { return p.Y / p.X } // Angle returns the angle in radians [0,2PI) between the x-axis and OP. func Angle(p math32.Vector2) float32 { return AngleNorm(math32.Atan2(p.Y, p.X)) } // todo: use this for our AngleTo // AngleBetween returns the angle between OP and OQ. func AngleBetween(p, q math32.Vector2) float32 { return math32.Atan2(p.Cross(q), p.Dot(q)) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/minify // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import "github.com/tdewolff/parse/v2/strconv" // MaxInt is the maximum value of int. const MaxInt = int(^uint(0) >> 1) // MinInt is the minimum value of int. const MinInt = -MaxInt - 1 // MinifyDecimal minifies a given byte slice containing a decimal and removes superfluous characters. It differs from Number in that it does not parse exponents. // It does not parse or output exponents. prec is the number of significant digits. When prec is zero it will keep all digits. Only digits after the dot can be removed to reach the number of significant digits. Very large number may thus have more significant digits. func MinifyDecimal(num []byte, prec int) []byte { if len(num) <= 1 { return num } // omit first + and register mantissa start and end, whether it's negative and the exponent neg := false start := 0 dot := -1 end := len(num) if 0 < end && (num[0] == '+' || num[0] == '-') { if num[0] == '-' { neg = true } start++ } for i, c := range num[start:] { if c == '.' { dot = start + i break } } if dot == -1 { dot = end } // trim leading zeros but leave at least one digit for start < end-1 && num[start] == '0' { start++ } // trim trailing zeros i := end - 1 for ; dot < i; i-- { if num[i] != '0' { end = i + 1 break } } if i == dot { end = dot if start == end { num[start] = '0' return num[start : start+1] } } else if start == end-1 && num[start] == '0' { return num[start:end] } // apply precision if 0 < prec && dot <= start+prec { precEnd := start + prec + 1 // include dot if dot == start { // for numbers like .012 digit := start + 1 for digit < end && num[digit] == '0' { digit++ } precEnd = digit + prec } if precEnd < end { end = precEnd // process either an increase from a lesser significant decimal (>= 5) // or remove trailing zeros after the dot, or both i := end - 1 inc := '5' <= num[end] for ; start < i; i-- { if i == dot { // no-op } else if inc && num[i] != '9' { num[i]++ inc = false break } else if inc && i < dot { // end inc for integer num[i] = '0' } else if !inc && (i < dot || num[i] != '0') { break } } if i < dot { end = dot } else { end = i + 1 } if inc { if dot == start && end == start+1 { num[start] = '1' } else if num[start] == '9' { num[start] = '1' num[start+1] = '0' end++ } else { num[start]++ } } } } if neg { start-- num[start] = '-' } return num[start:end] } // MinifyNumber minifies a given byte slice containing a number and removes superfluous characters. func MinifyNumber(num []byte, prec int) []byte { if len(num) <= 1 { return num } // omit first + and register mantissa start and end, whether it's negative and the exponent neg := false start := 0 dot := -1 end := len(num) origExp := 0 if num[0] == '+' || num[0] == '-' { if num[0] == '-' { neg = true } start++ } for i, c := range num[start:] { if c == '.' { dot = start + i } else if c == 'e' || c == 'E' { end = start + i i += start + 1 if i < len(num) && num[i] == '+' { i++ } if tmpOrigExp, n := strconv.ParseInt(num[i:]); 0 < n && int64(MinInt) <= tmpOrigExp && tmpOrigExp <= int64(MaxInt) { // range checks for when int is 32 bit origExp = int(tmpOrigExp) } else { return num } break } } if dot == -1 { dot = end } // trim leading zeros but leave at least one digit for start < end-1 && num[start] == '0' { start++ } // trim trailing zeros i := end - 1 for ; dot < i; i-- { if num[i] != '0' { end = i + 1 break } } if i == dot { end = dot if start == end { num[start] = '0' return num[start : start+1] } } else if start == end-1 && num[start] == '0' { return num[start:end] } // apply precision if 0 < prec { //&& (dot <= start+prec || start+prec+1 < dot || 0 < origExp) { // don't minify 9 to 10, but do 999 to 1e3 and 99e1 to 1e3 precEnd := start + prec if dot == start { // for numbers like .012 digit := start + 1 for digit < end && num[digit] == '0' { digit++ } precEnd = digit + prec } else if dot < precEnd { // for numbers where precision will include the dot precEnd++ } if precEnd < end && (dot < end || 1 < dot-precEnd+origExp) { // do not minify 9=>10 or 99=>100 or 9e1=>1e2 (but 90), but 999=>1e3 and 99e1=>1e3 end = precEnd inc := '5' <= num[end] if dot == end { inc = end+1 < len(num) && '5' <= num[end+1] } if precEnd < dot { origExp += dot - precEnd dot = precEnd } // process either an increase from a lesser significant decimal (>= 5) // and remove trailing zeros i := end - 1 for ; start < i; i-- { if i == dot { // no-op } else if inc && num[i] != '9' { num[i]++ inc = false break } else if !inc && num[i] != '0' { break } } end = i + 1 if end < dot { origExp += dot - end dot = end } if inc { // single digit left if dot == start { num[start] = '1' dot = start + 1 } else if num[start] == '9' { num[start] = '1' origExp++ } else { num[start]++ } } } } // n is the number of significant digits // normExp would be the exponent if it were normalised (0.1 <= f < 1) n := 0 normExp := 0 if dot == start { for i = dot + 1; i < end; i++ { if num[i] != '0' { n = end - i normExp = dot - i + 1 break } } } else if dot == end { normExp = end - start for i = end - 1; start <= i; i-- { if num[i] != '0' { n = i + 1 - start end = i + 1 break } } } else { n = end - start - 1 normExp = dot - start } if origExp < 0 && (normExp < MinInt-origExp || normExp-n < MinInt-origExp) || 0 < origExp && (MaxInt-origExp < normExp || MaxInt-origExp < normExp-n) { return num // exponent overflow } normExp += origExp // intExp would be the exponent if it were an integer intExp := normExp - n lenIntExp := strconv.LenInt(int64(intExp)) lenNormExp := strconv.LenInt(int64(normExp)) // there are three cases to consider when printing the number // case 1: without decimals and with a positive exponent (large numbers: 5e4) // case 2: with decimals and with a negative exponent (small numbers with many digits: .123456e-4) // case 3: with decimals and without an exponent (around zero: 5.6) // case 4: without decimals and with a negative exponent (small numbers: 123456e-9) if n <= normExp { // case 1: print number with positive exponent if dot < end { // remove dot, either from the front or copy the smallest part if dot == start { start = end - n } else if dot-start < end-dot-1 { copy(num[start+1:], num[start:dot]) start++ } else { copy(num[dot:], num[dot+1:end]) end-- } } if n+3 <= normExp { num[end] = 'e' end++ for i := end + lenIntExp - 1; end <= i; i-- { num[i] = byte(intExp%10) + '0' intExp /= 10 } end += lenIntExp } else if n+2 == normExp { num[end] = '0' num[end+1] = '0' end += 2 } else if n+1 == normExp { num[end] = '0' end++ } } else if normExp < -3 && lenNormExp < lenIntExp && dot < end { // case 2: print normalized number (0.1 <= f < 1) zeroes := -normExp + origExp if 0 < zeroes { copy(num[start+1:], num[start+1+zeroes:end]) end -= zeroes } else if zeroes < 0 { copy(num[start+1:], num[start:dot]) num[start] = '.' } num[end] = 'e' num[end+1] = '-' end += 2 for i := end + lenNormExp - 1; end <= i; i-- { num[i] = -byte(normExp%10) + '0' normExp /= 10 } end += lenNormExp } else if -lenIntExp-1 <= normExp { // case 3: print number without exponent zeroes := -normExp if 0 < zeroes { // dot placed at the front and negative exponent, adding zeroes newDot := end - n - zeroes - 1 if newDot != dot { d := start - newDot if 0 < d { if dot < end { // copy original digits after the dot towards the end copy(num[dot+1+d:], num[dot+1:end]) if start < dot { // copy original digits before the dot towards the end copy(num[start+d+1:], num[start:dot]) } } else if start < dot { // copy original digits before the dot towards the end copy(num[start+d:], num[start:dot]) } newDot = start end += d } else { start += -d } num[newDot] = '.' for i := 0; i < zeroes; i++ { num[newDot+1+i] = '0' } } } else { // dot placed in the middle of the number if dot == start { // when there are zeroes after the dot dot = end - n - 1 start = dot } else if end <= dot { // when input has no dot in it dot = end end++ } newDot := start + normExp // move digits between dot and newDot towards the end if dot < newDot { copy(num[dot:], num[dot+1:newDot+1]) } else if newDot < dot { copy(num[newDot+1:], num[newDot:dot]) } num[newDot] = '.' } } else { // case 4: print number with negative exponent // find new end, considering moving numbers to the front, removing the dot and increasing the length of the exponent newEnd := end if dot == start { newEnd = start + n } else { newEnd-- } newEnd += 2 + lenIntExp exp := intExp lenExp := lenIntExp if newEnd < len(num) { // it saves space to convert the decimal to an integer and decrease the exponent if dot < end { if dot == start { copy(num[start:], num[end-n:end]) end = start + n } else { copy(num[dot:], num[dot+1:end]) end-- } } } else { // it does not save space and will panic, so we revert to the original representation exp = origExp lenExp = 1 if origExp <= -10 || 10 <= origExp { lenExp = strconv.LenInt(int64(origExp)) } } num[end] = 'e' num[end+1] = '-' end += 2 for i := end + lenExp - 1; end <= i; i-- { num[i] = -byte(exp%10) + '0' exp /= 10 } end += lenExp } if neg { start-- num[start] = '-' } return num[start:end] } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import ( "bytes" "encoding/gob" "slices" "cogentcore.org/core/math32" ) // ArcToCubeImmediate causes ArcTo commands to be immediately converted into // corresponding CubeTo commands, instead of doing this later. // This is faster than using [Path.ReplaceArcs], but when rendering to SVG // it might be better to turn this off in order to preserve the logical structure // of the arcs in the SVG output. var ArcToCubeImmediate = true // Path is a collection of MoveTo, LineTo, QuadTo, CubeTo, ArcTo, and Close // commands, each followed the float32 coordinate data for it. // To enable support bidirectional processing, the command verb is also added // to the end of the coordinate data as well. // The last two coordinate values are the end point position of the pen after // the action (x,y). // QuadTo defines one control point (x,y) in between. // CubeTo defines two control points. // ArcTo defines (rx,ry,phi,large+sweep) i.e. the radius in x and y, // its rotation (in radians) and the large and sweep booleans in one float32. // While ArcTo can be converted to CubeTo, it is useful for the path intersection // computation. // Only valid commands are appended, so that LineTo has a non-zero length, // QuadTo's and CubeTo's control point(s) don't (both) overlap with the start // and end point. type Path []float32 func New() *Path { return &Path{} } // Commands const ( MoveTo float32 = 0 LineTo float32 = 1 QuadTo float32 = 2 CubeTo float32 = 3 ArcTo float32 = 4 Close float32 = 5 ) var cmdLens = [6]int{4, 4, 6, 8, 8, 4} // CmdLen returns the overall length of the command, including // the command op itself. func CmdLen(cmd float32) int { return cmdLens[int(cmd)] } // ToArcFlags converts to the largeArc and sweep boolean flags given its value in the path. func ToArcFlags(cmd float32) (bool, bool) { large := (cmd == 1.0 || cmd == 3.0) sweep := (cmd == 2.0 || cmd == 3.0) return large, sweep } // fromArcFlags converts the largeArc and sweep boolean flags to a value stored in the path. func fromArcFlags(large, sweep bool) float32 { f := float32(0.0) if large { f += 1.0 } if sweep { f += 2.0 } return f } // Paths is a collection of Path elements. type Paths []Path // Empty returns true if the set of paths is empty. func (ps Paths) Empty() bool { for _, p := range ps { if !p.Empty() { return false } } return true } // Reset clears the path but retains the same memory. // This can be used in loops where you append and process // paths every iteration, and avoid new memory allocations. func (p *Path) Reset() { *p = (*p)[:0] } // GobEncode implements the gob interface. func (p Path) GobEncode() ([]byte, error) { b := bytes.Buffer{} enc := gob.NewEncoder(&b) if err := enc.Encode(p); err != nil { return nil, err } return b.Bytes(), nil } // GobDecode implements the gob interface. func (p *Path) GobDecode(b []byte) error { dec := gob.NewDecoder(bytes.NewReader(b)) return dec.Decode(p) } // Empty returns true if p is an empty path or consists of only MoveTos and Closes. func (p Path) Empty() bool { return len(p) <= CmdLen(MoveTo) } // Equals returns true if p and q are equal within tolerance Epsilon. func (p Path) Equals(q Path) bool { if len(p) != len(q) { return false } for i := 0; i < len(p); i++ { if !Equal(p[i], q[i]) { return false } } return true } // Sane returns true if the path is sane, ie. it does not have NaN or infinity values. func (p Path) Sane() bool { sane := func(x float32) bool { return !math32.IsNaN(x) && !math32.IsInf(x, 0.0) } for i := 0; i < len(p); { cmd := p[i] i += CmdLen(cmd) if !sane(p[i-3]) || !sane(p[i-2]) { return false } switch cmd { case QuadTo: if !sane(p[i-5]) || !sane(p[i-4]) { return false } case CubeTo, ArcTo: if !sane(p[i-7]) || !sane(p[i-6]) || !sane(p[i-5]) || !sane(p[i-4]) { return false } } } return true } // Same returns true if p and q are equal shapes within tolerance Epsilon. // Path q may start at an offset into path p or may be in the reverse direction. func (p Path) Same(q Path) bool { // TODO: improve, does not handle subpaths or Close vs LineTo if len(p) != len(q) { return false } qr := q.Reverse() // TODO: can we do without? for j := 0; j < len(q); { equal := true for i := 0; i < len(p); i++ { if !Equal(p[i], q[(j+i)%len(q)]) { equal = false break } } if equal { return true } // backwards equal = true for i := 0; i < len(p); i++ { if !Equal(p[i], qr[(j+i)%len(qr)]) { equal = false break } } if equal { return true } j += CmdLen(q[j]) } return false } // Closed returns true if the last subpath of p is a closed path. func (p Path) Closed() bool { return 0 < len(p) && p[len(p)-1] == Close } // PointClosed returns true if the last subpath of p is a closed path // and the close command is a point and not a line. func (p Path) PointClosed() bool { return 6 < len(p) && p[len(p)-1] == Close && Equal(p[len(p)-7], p[len(p)-3]) && Equal(p[len(p)-6], p[len(p)-2]) } // HasSubpaths returns true when path p has subpaths. // TODO: naming right? A simple path would not self-intersect. // Add IsXMonotone and IsFlat as well? func (p Path) HasSubpaths() bool { for i := 0; i < len(p); { if p[i] == MoveTo && i != 0 { return true } i += CmdLen(p[i]) } return false } // Clone returns a copy of p. func (p Path) Clone() Path { return slices.Clone(p) } // CopyTo returns a copy of p, using the memory of path q. func (p Path) CopyTo(q Path) Path { if q == nil || len(q) < len(p) { q = make(Path, len(p)) } else { q = q[:len(p)] } copy(q, p) return q } // Len returns the number of commands in the path. func (p Path) Len() int { n := 0 for i := 0; i < len(p); { i += CmdLen(p[i]) n++ } return n } // Append appends path q to p and returns the extended path p. func (p Path) Append(qs ...Path) Path { if p.Empty() { p = Path{} } for _, q := range qs { if !q.Empty() { p = append(p, q...) } } return p } // Join joins path q to p and returns the extended path p // (or q if p is empty). It's like executing the commands // in q to p in sequence, where if the first MoveTo of q // doesn't coincide with p, or if p ends in Close, // it will fallback to appending the paths. func (p Path) Join(q Path) Path { if q.Empty() { return p } else if p.Empty() { return q } if p[len(p)-1] == Close || !Equal(p[len(p)-3], q[1]) || !Equal(p[len(p)-2], q[2]) { return append(p, q...) } d := q[CmdLen(MoveTo):] // add the first command through the command functions to use the optimization features // q is not empty, so starts with a MoveTo followed by other commands cmd := d[0] switch cmd { case MoveTo: p.MoveTo(d[1], d[2]) case LineTo: p.LineTo(d[1], d[2]) case QuadTo: p.QuadTo(d[1], d[2], d[3], d[4]) case CubeTo: p.CubeTo(d[1], d[2], d[3], d[4], d[5], d[6]) case ArcTo: large, sweep := ToArcFlags(d[4]) p.ArcTo(d[1], d[2], d[3], large, sweep, d[5], d[6]) case Close: p.Close() } i := len(p) end := p.StartPos() p = append(p, d[CmdLen(cmd):]...) // repair close commands for i < len(p) { cmd := p[i] if cmd == MoveTo { break } else if cmd == Close { p[i+1] = end.X p[i+2] = end.Y break } i += CmdLen(cmd) } return p } // Pos returns the current position of the path, // which is the end point of the last command. func (p Path) Pos() math32.Vector2 { if 0 < len(p) { return math32.Vec2(p[len(p)-3], p[len(p)-2]) } return math32.Vector2{} } // StartPos returns the start point of the current subpath, // i.e. it returns the position of the last MoveTo command. func (p Path) StartPos() math32.Vector2 { for i := len(p); 0 < i; { cmd := p[i-1] if cmd == MoveTo { return math32.Vec2(p[i-3], p[i-2]) } i -= CmdLen(cmd) } return math32.Vector2{} } // Coords returns all the coordinates of the segment // start/end points. It omits zero-length Closes. func (p Path) Coords() []math32.Vector2 { coords := []math32.Vector2{} for i := 0; i < len(p); { cmd := p[i] i += CmdLen(cmd) if len(coords) == 0 || cmd != Close || !EqualPoint(coords[len(coords)-1], math32.Vec2(p[i-3], p[i-2])) { coords = append(coords, math32.Vec2(p[i-3], p[i-2])) } } return coords } /////// Accessors // EndPoint returns the end point for MoveTo, LineTo, and Close commands, // where the command is at index i. func (p Path) EndPoint(i int) math32.Vector2 { return math32.Vec2(p[i+1], p[i+2]) } // QuadToPoints returns the control point and end for QuadTo command, // where the command is at index i. func (p Path) QuadToPoints(i int) (cp, end math32.Vector2) { return math32.Vec2(p[i+1], p[i+2]), math32.Vec2(p[i+3], p[i+4]) } // CubeToPoints returns the cp1, cp2, and end for CubeTo command, // where the command is at index i. func (p Path) CubeToPoints(i int) (cp1, cp2, end math32.Vector2) { return math32.Vec2(p[i+1], p[i+2]), math32.Vec2(p[i+3], p[i+4]), math32.Vec2(p[i+5], p[i+6]) } // ArcToPoints returns the rx, ry, phi, large, sweep values for ArcTo command, // where the command is at index i. func (p Path) ArcToPoints(i int) (rx, ry, phi float32, large, sweep bool, end math32.Vector2) { rx = p[i+1] ry = p[i+2] phi = p[i+3] large, sweep = ToArcFlags(p[i+4]) end = math32.Vec2(p[i+5], p[i+6]) return } /////// Constructors // MoveTo moves the path to (x,y) without connecting the path. // It starts a new independent subpath. Multiple subpaths can be useful // when negating parts of a previous path by overlapping it with a path // in the opposite direction. The behaviour for overlapping paths depends // on the FillRules. func (p *Path) MoveTo(x, y float32) { if 0 < len(*p) && (*p)[len(*p)-1] == MoveTo { (*p)[len(*p)-3] = x (*p)[len(*p)-2] = y return } *p = append(*p, MoveTo, x, y, MoveTo) } // LineTo adds a linear path to (x,y). func (p *Path) LineTo(x, y float32) { start := p.Pos() end := math32.Vector2{x, y} if EqualPoint(start, end) { return } else if CmdLen(LineTo) <= len(*p) && (*p)[len(*p)-1] == LineTo { prevStart := math32.Vector2{} if CmdLen(LineTo) < len(*p) { prevStart = math32.Vec2((*p)[len(*p)-CmdLen(LineTo)-3], (*p)[len(*p)-CmdLen(LineTo)-2]) } // divide by length^2 since otherwise the perpdot between very small segments may be // below Epsilon da := start.Sub(prevStart) db := end.Sub(start) div := da.Cross(db) if length := da.Length() * db.Length(); Equal(div/length, 0.0) { // lines are parallel extends := false if da.Y < da.X { extends = math32.Signbit(da.X) == math32.Signbit(db.X) } else { extends = math32.Signbit(da.Y) == math32.Signbit(db.Y) } if extends { //if Equal(end.Sub(start).AngleBetween(start.Sub(prevStart)), 0.0) { (*p)[len(*p)-3] = x (*p)[len(*p)-2] = y return } } } if len(*p) == 0 { p.MoveTo(0.0, 0.0) } else if (*p)[len(*p)-1] == Close { p.MoveTo((*p)[len(*p)-3], (*p)[len(*p)-2]) } *p = append(*p, LineTo, end.X, end.Y, LineTo) } // QuadTo adds a quadratic Bézier path with control point (cpx,cpy) and end point (x,y). func (p *Path) QuadTo(cpx, cpy, x, y float32) { start := p.Pos() cp := math32.Vector2{cpx, cpy} end := math32.Vector2{x, y} if EqualPoint(start, end) && EqualPoint(start, cp) { return } else if !EqualPoint(start, end) && (EqualPoint(start, cp) || AngleEqual(AngleBetween(end.Sub(start), cp.Sub(start)), 0.0)) && (EqualPoint(end, cp) || AngleEqual(AngleBetween(end.Sub(start), end.Sub(cp)), 0.0)) { p.LineTo(end.X, end.Y) return } if len(*p) == 0 { p.MoveTo(0.0, 0.0) } else if (*p)[len(*p)-1] == Close { p.MoveTo((*p)[len(*p)-3], (*p)[len(*p)-2]) } *p = append(*p, QuadTo, cp.X, cp.Y, end.X, end.Y, QuadTo) } // CubeTo adds a cubic Bézier path with control points // (cpx1,cpy1) and (cpx2,cpy2) and end point (x,y). func (p *Path) CubeTo(cpx1, cpy1, cpx2, cpy2, x, y float32) { start := p.Pos() cp1 := math32.Vector2{cpx1, cpy1} cp2 := math32.Vector2{cpx2, cpy2} end := math32.Vector2{x, y} if EqualPoint(start, end) && EqualPoint(start, cp1) && EqualPoint(start, cp2) { return } else if !EqualPoint(start, end) && (EqualPoint(start, cp1) || EqualPoint(end, cp1) || AngleEqual(AngleBetween(end.Sub(start), cp1.Sub(start)), 0.0) && AngleEqual(AngleBetween(end.Sub(start), end.Sub(cp1)), 0.0)) && (EqualPoint(start, cp2) || EqualPoint(end, cp2) || AngleEqual(AngleBetween(end.Sub(start), cp2.Sub(start)), 0.0) && AngleEqual(AngleBetween(end.Sub(start), end.Sub(cp2)), 0.0)) { p.LineTo(end.X, end.Y) return } if len(*p) == 0 { p.MoveTo(0.0, 0.0) } else if (*p)[len(*p)-1] == Close { p.MoveTo((*p)[len(*p)-3], (*p)[len(*p)-2]) } *p = append(*p, CubeTo, cp1.X, cp1.Y, cp2.X, cp2.Y, end.X, end.Y, CubeTo) } // ArcTo adds an arc with radii rx and ry, with rot the counter clockwise // rotation with respect to the coordinate system in radians, large and sweep booleans // (see https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Arcs), // and (x,y) the end position of the pen. The start position of the pen was // given by a previous command's end point. func (p *Path) ArcTo(rx, ry, rot float32, large, sweep bool, x, y float32) { start := p.Pos() end := math32.Vector2{x, y} if EqualPoint(start, end) { return } if Equal(rx, 0.0) || math32.IsInf(rx, 0) || Equal(ry, 0.0) || math32.IsInf(ry, 0) { p.LineTo(end.X, end.Y) return } rx = math32.Abs(rx) ry = math32.Abs(ry) if Equal(rx, ry) { rot = 0.0 // circle } else if rx < ry { rx, ry = ry, rx rot += math32.Pi / 2.0 } phi := AngleNorm(rot) if math32.Pi <= phi { // phi is canonical within 0 <= phi < 180 phi -= math32.Pi } // scale ellipse if rx and ry are too small lambda := EllipseRadiiCorrection(start, rx, ry, phi, end) if lambda > 1.0 { rx *= lambda ry *= lambda } if len(*p) == 0 { p.MoveTo(0.0, 0.0) } else if (*p)[len(*p)-1] == Close { p.MoveTo((*p)[len(*p)-3], (*p)[len(*p)-2]) } if ArcToCubeImmediate { for _, bezier := range ellipseToCubicBeziers(start, rx, ry, phi, large, sweep, end) { p.CubeTo(bezier[1].X, bezier[1].Y, bezier[2].X, bezier[2].Y, bezier[3].X, bezier[3].Y) } } else { *p = append(*p, ArcTo, rx, ry, phi, fromArcFlags(large, sweep), end.X, end.Y, ArcTo) } } // ArcToDeg is a version of [Path.ArcTo] with the angle in degrees instead of radians. // It adds an arc with radii rx and ry, with rot the counter clockwise // rotation with respect to the coordinate system in degrees, large and sweep booleans // (see https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Arcs), // and (x,y) the end position of the pen. The start position of the pen was // given by a previous command's end point. func (p *Path) ArcToDeg(rx, ry, rot float32, large, sweep bool, x, y float32) { p.ArcTo(rx, ry, math32.DegToRad(rot), large, sweep, x, y) } // Arc adds an elliptical arc with radii rx and ry, with rot the // counter clockwise rotation in radians, and theta0 and theta1 // the angles in radians of the ellipse (before rot is applies) // between which the arc will run. If theta0 < theta1, // the arc will run in a CCW direction. If the difference between // theta0 and theta1 is bigger than 360 degrees, one full circle // will be drawn and the remaining part of diff % 360, // e.g. a difference of 810 degrees will draw one full circle // and an arc over 90 degrees. func (p *Path) Arc(rx, ry, phi, theta0, theta1 float32) { dtheta := math32.Abs(theta1 - theta0) sweep := theta0 < theta1 large := math32.Mod(dtheta, 2.0*math32.Pi) > math32.Pi p0 := EllipsePos(rx, ry, phi, 0.0, 0.0, theta0) p1 := EllipsePos(rx, ry, phi, 0.0, 0.0, theta1) start := p.Pos() center := start.Sub(p0) if dtheta >= 2.0*math32.Pi { startOpposite := center.Sub(p0) p.ArcTo(rx, ry, phi, large, sweep, startOpposite.X, startOpposite.Y) p.ArcTo(rx, ry, phi, large, sweep, start.X, start.Y) if Equal(math32.Mod(dtheta, 2.0*math32.Pi), 0.0) { return } } end := center.Add(p1) p.ArcTo(rx, ry, phi, large, sweep, end.X, end.Y) } // ArcDeg is a version of [Path.Arc] that uses degrees instead of radians, // to add an elliptical arc with radii rx and ry, with rot the // counter clockwise rotation in degrees, and theta0 and theta1 // the angles in degrees of the ellipse (before rot is applied) // between which the arc will run. func (p *Path) ArcDeg(rx, ry, rot, theta0, theta1 float32) { p.Arc(rx, ry, math32.DegToRad(rot), math32.DegToRad(theta0), math32.DegToRad(theta1)) } // Close closes a (sub)path with a LineTo to the start of the path // (the most recent MoveTo command). It also signals the path closes // as opposed to being just a LineTo command, which can be significant // for stroking purposes for example. func (p *Path) Close() { if len(*p) == 0 || (*p)[len(*p)-1] == Close { // already closed or empty return } else if (*p)[len(*p)-1] == MoveTo { // remove MoveTo + Close *p = (*p)[:len(*p)-CmdLen(MoveTo)] return } end := p.StartPos() if (*p)[len(*p)-1] == LineTo && Equal((*p)[len(*p)-3], end.X) && Equal((*p)[len(*p)-2], end.Y) { // replace LineTo by Close if equal (*p)[len(*p)-1] = Close (*p)[len(*p)-CmdLen(LineTo)] = Close return } else if (*p)[len(*p)-1] == LineTo { // replace LineTo by Close if equidirectional extension start := math32.Vec2((*p)[len(*p)-3], (*p)[len(*p)-2]) prevStart := math32.Vector2{} if CmdLen(LineTo) < len(*p) { prevStart = math32.Vec2((*p)[len(*p)-CmdLen(LineTo)-3], (*p)[len(*p)-CmdLen(LineTo)-2]) } if Equal(AngleBetween(end.Sub(start), start.Sub(prevStart)), 0.0) { (*p)[len(*p)-CmdLen(LineTo)] = Close (*p)[len(*p)-3] = end.X (*p)[len(*p)-2] = end.Y (*p)[len(*p)-1] = Close return } } *p = append(*p, Close, end.X, end.Y, Close) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import ( "cogentcore.org/core/math32" ) // Scanner returns a path scanner. func (p Path) Scanner() *Scanner { return &Scanner{p, -1} } // ReverseScanner returns a path scanner in reverse order. func (p Path) ReverseScanner() ReverseScanner { return ReverseScanner{p, len(p)} } // Scanner scans the path. type Scanner struct { p Path i int } // Scan scans a new path segment and should be called before the other methods. func (s *Scanner) Scan() bool { if s.i+1 < len(s.p) { s.i += CmdLen(s.p[s.i+1]) return true } return false } // Cmd returns the current path segment command. func (s *Scanner) Cmd() float32 { return s.p[s.i] } // Values returns the current path segment values. func (s *Scanner) Values() []float32 { return s.p[s.i-CmdLen(s.p[s.i])+2 : s.i] } // Start returns the current path segment start position. func (s *Scanner) Start() math32.Vector2 { i := s.i - CmdLen(s.p[s.i]) if i == -1 { return math32.Vector2{} } return math32.Vector2{s.p[i-2], s.p[i-1]} } // CP1 returns the first control point for quadratic and cubic Béziers. func (s *Scanner) CP1() math32.Vector2 { if s.p[s.i] != QuadTo && s.p[s.i] != CubeTo { panic("must be quadratic or cubic Bézier") } i := s.i - CmdLen(s.p[s.i]) + 1 return math32.Vector2{s.p[i+1], s.p[i+2]} } // CP2 returns the second control point for cubic Béziers. func (s *Scanner) CP2() math32.Vector2 { if s.p[s.i] != CubeTo { panic("must be cubic Bézier") } i := s.i - CmdLen(s.p[s.i]) + 1 return math32.Vector2{s.p[i+3], s.p[i+4]} } // Arc returns the arguments for arcs (rx,ry,rot,large,sweep). func (s *Scanner) Arc() (float32, float32, float32, bool, bool) { if s.p[s.i] != ArcTo { panic("must be arc") } i := s.i - CmdLen(s.p[s.i]) + 1 large, sweep := ToArcFlags(s.p[i+4]) return s.p[i+1], s.p[i+2], s.p[i+3], large, sweep } // End returns the current path segment end position. func (s *Scanner) End() math32.Vector2 { return math32.Vector2{s.p[s.i-2], s.p[s.i-1]} } // Path returns the current path segment. func (s *Scanner) Path() Path { p := Path{} p.MoveTo(s.Start().X, s.Start().Y) switch s.Cmd() { case LineTo: p.LineTo(s.End().X, s.End().Y) case QuadTo: p.QuadTo(s.CP1().X, s.CP1().Y, s.End().X, s.End().Y) case CubeTo: p.CubeTo(s.CP1().X, s.CP1().Y, s.CP2().X, s.CP2().Y, s.End().X, s.End().Y) case ArcTo: rx, ry, rot, large, sweep := s.Arc() p.ArcTo(rx, ry, rot, large, sweep, s.End().X, s.End().Y) } return p } // ReverseScanner scans the path in reverse order. type ReverseScanner struct { p Path i int } // Scan scans a new path segment and should be called before the other methods. func (s *ReverseScanner) Scan() bool { if 0 < s.i { s.i -= CmdLen(s.p[s.i-1]) return true } return false } // Cmd returns the current path segment command. func (s *ReverseScanner) Cmd() float32 { return s.p[s.i] } // Values returns the current path segment values. func (s *ReverseScanner) Values() []float32 { return s.p[s.i+1 : s.i+CmdLen(s.p[s.i])-1] } // Start returns the current path segment start position. func (s *ReverseScanner) Start() math32.Vector2 { if s.i == 0 { return math32.Vector2{} } return math32.Vector2{s.p[s.i-3], s.p[s.i-2]} } // CP1 returns the first control point for quadratic and cubic Béziers. func (s *ReverseScanner) CP1() math32.Vector2 { if s.p[s.i] != QuadTo && s.p[s.i] != CubeTo { panic("must be quadratic or cubic Bézier") } return math32.Vector2{s.p[s.i+1], s.p[s.i+2]} } // CP2 returns the second control point for cubic Béziers. func (s *ReverseScanner) CP2() math32.Vector2 { if s.p[s.i] != CubeTo { panic("must be cubic Bézier") } return math32.Vector2{s.p[s.i+3], s.p[s.i+4]} } // Arc returns the arguments for arcs (rx,ry,rot,large,sweep). func (s *ReverseScanner) Arc() (float32, float32, float32, bool, bool) { if s.p[s.i] != ArcTo { panic("must be arc") } large, sweep := ToArcFlags(s.p[s.i+4]) return s.p[s.i+1], s.p[s.i+2], s.p[s.i+3], large, sweep } // End returns the current path segment end position. func (s *ReverseScanner) End() math32.Vector2 { i := s.i + CmdLen(s.p[s.i]) return math32.Vector2{s.p[i-3], s.p[i-2]} } // Path returns the current path segment. func (s *ReverseScanner) Path() Path { p := Path{} p.MoveTo(s.Start().X, s.Start().Y) switch s.Cmd() { case LineTo: p.LineTo(s.End().X, s.End().Y) case QuadTo: p.QuadTo(s.CP1().X, s.CP1().Y, s.End().X, s.End().Y) case CubeTo: p.CubeTo(s.CP1().X, s.CP1().Y, s.CP2().X, s.CP2().Y, s.End().X, s.End().Y) case ArcTo: rx, ry, rot, large, sweep := s.Arc() p.ArcTo(rx, ry, rot, large, sweep, s.End().X, s.End().Y) } return p } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import ( "cogentcore.org/core/math32" "cogentcore.org/core/styles/sides" ) // Line adds a line segment of from (x1,y1) to (x2,y2). func (p *Path) Line(x1, y1, x2, y2 float32) *Path { if Equal(x1, x2) && Equal(y1, y2) { return p } p.MoveTo(x1, y1) p.LineTo(x2, y2) return p } // Polyline adds multiple connected lines, with no final Close. func (p *Path) Polyline(points ...math32.Vector2) *Path { sz := len(points) if sz < 2 { return p } p.MoveTo(points[0].X, points[0].Y) for i := 1; i < sz; i++ { p.LineTo(points[i].X, points[i].Y) } return p } // Polygon adds multiple connected lines with a final Close. func (p *Path) Polygon(points ...math32.Vector2) *Path { p.Polyline(points...) p.Close() return p } // Rectangle adds a rectangle of width w and height h. func (p *Path) Rectangle(x, y, w, h float32) *Path { if Equal(w, 0.0) || Equal(h, 0.0) { return p } p.MoveTo(x, y) p.LineTo(x+w, y) p.LineTo(x+w, y+h) p.LineTo(x, y+h) p.Close() return p } // RoundedRectangle adds a rectangle of width w and height h // with rounded corners of radius r. A negative radius will cast // the corners inwards (i.e. concave). func (p *Path) RoundedRectangle(x, y, w, h, r float32) *Path { if Equal(w, 0.0) || Equal(h, 0.0) { return p } else if Equal(r, 0.0) { return p.Rectangle(x, y, w, h) } sweep := true if r < 0.0 { sweep = false r = -r } r = math32.Min(r, w/2.0) r = math32.Min(r, h/2.0) p.MoveTo(x, y+r) p.ArcTo(r, r, 0.0, false, sweep, x+r, y) p.LineTo(x+w-r, y) p.ArcTo(r, r, 0.0, false, sweep, x+w, y+r) p.LineTo(x+w, y+h-r) p.ArcTo(r, r, 0.0, false, sweep, x+w-r, y+h) p.LineTo(x+r, y+h) p.ArcTo(r, r, 0.0, false, sweep, x, y+h-r) p.Close() return p } // RoundedRectangleSides 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. // This version uses the Arc elliptical arc function. func (p *Path) RoundedRectangleSides(x, y, w, h float32, r sides.Floats) *Path { // 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 ) p.MoveTo(xtl, ytli) if r.Top != 0 { p.ArcTo(r.Top, r.Top, 0, false, true, xtli, ytl) } p.LineTo(xtri, ytr) if r.Right != 0 { p.ArcTo(r.Right, r.Right, 0, false, true, xbr, ytri) } p.LineTo(xbr, ybri) if r.Bottom != 0 { p.ArcTo(r.Bottom, r.Bottom, 0, false, true, xbri, ybl) } p.LineTo(xbli, ybl) if r.Left != 0 { p.ArcTo(r.Left, r.Left, 0, false, true, xtl, ybli) } p.Close() return p } // BeveledRectangle adds a rectangle of width w and height h // with beveled corners at distance r from the corner. func (p *Path) BeveledRectangle(x, y, w, h, r float32) *Path { if Equal(w, 0.0) || Equal(h, 0.0) { return p } else if Equal(r, 0.0) { return p.Rectangle(x, y, w, h) } r = math32.Abs(r) r = math32.Min(r, w/2.0) r = math32.Min(r, h/2.0) p.MoveTo(x, y+r) p.LineTo(x+r, y) p.LineTo(x+w-r, y) p.LineTo(x+w, y+r) p.LineTo(x+w, y+h-r) p.LineTo(x+w-r, y+h) p.LineTo(x+r, y+h) p.LineTo(x, y+h-r) p.Close() return p } // Circle adds a circle at given center coordinates of radius r. func (p *Path) Circle(cx, cy, r float32) *Path { return p.Ellipse(cx, cy, r, r) } // Ellipse adds an ellipse at given center coordinates of radii rx and ry. func (p *Path) Ellipse(cx, cy, rx, ry float32) *Path { if Equal(rx, 0.0) || Equal(ry, 0.0) { return p } p.MoveTo(cx+rx, cy+(ry*0.001)) p.ArcTo(rx, ry, 0.0, false, true, cx-rx, cy) p.ArcTo(rx, ry, 0.0, false, true, cx+rx, cy) p.Close() return p } // CircularArc adds a circular arc centered at given coordinates with radius r // and theta0 and theta1 as the angles in degrees of the ellipse // (before rot is applied) between which the arc will run. // If theta0 < theta1, the arc will run in a CCW direction. // If the difference between theta0 and theta1 is bigger than 360 degrees, // one full circle will be drawn and the remaining part of diff % 360, // e.g. a difference of 810 degrees will draw one full circle and an arc // over 90 degrees. func (p *Path) CircularArc(x, y, r, theta0, theta1 float32) *Path { return p.EllipticalArc(x, y, r, r, 0, theta0, theta1) } // EllipticalArc adds an elliptical arc centered at given coordinates with // radii rx and ry, with rot the counter clockwise rotation in radians, // and theta0 and theta1 the angles in radians of the ellipse // (before rot is applied) between which the arc will run. // If theta0 < theta1, the arc will run in a CCW direction. // If the difference between theta0 and theta1 is bigger than 360 degrees, // one full circle will be drawn and the remaining part of diff % 360, // e.g. a difference of 810 degrees will draw one full circle and an arc // over 90 degrees. func (p *Path) EllipticalArc(x, y, rx, ry, rot, theta0, theta1 float32) *Path { sins, coss := math32.Sincos(theta0) sx := rx * coss sy := ry * sins p.MoveTo(x+sx, y+sy) p.Arc(rx, ry, rot, theta0, theta1) return p } // Triangle adds a triangle of radius r pointing upwards. func (p *Path) Triangle(r float32) *Path { return p.RegularPolygon(3, r, true) } // RegularPolygon adds a regular polygon with radius r. // It uses n vertices/edges, so when n approaches infinity // this will return a path that approximates a circle. // n must be 3 or more. The up boolean defines whether // the first point will point upwards or downwards. func (p *Path) RegularPolygon(n int, r float32, up bool) *Path { return p.RegularStarPolygon(n, 1, r, up) } // RegularStarPolygon adds a regular star polygon with radius r. // It uses n vertices of density d. This will result in a // self-intersection star in counter clockwise direction. // If n/2 < d the star will be clockwise and if n and d are not coprime // a regular polygon will be obtained, possible with multiple windings. // n must be 3 or more and d 2 or more. The up boolean defines whether // the first point will point upwards or downwards. func (p *Path) RegularStarPolygon(n, d int, r float32, up bool) *Path { if n < 3 || d < 1 || n == d*2 || Equal(r, 0.0) { return p } dtheta := 2.0 * math32.Pi / float32(n) theta0 := float32(0.5 * math32.Pi) if !up { theta0 += dtheta / 2.0 } for i := 0; i == 0 || i%n != 0; i += d { theta := theta0 + float32(i)*dtheta sintheta, costheta := math32.Sincos(theta) if i == 0 { p.MoveTo(r*costheta, r*sintheta) } else { p.LineTo(r*costheta, r*sintheta) } } p.Close() return p } // StarPolygon adds a star polygon of n points with alternating // radius R and r. The up boolean defines whether the first point // will be point upwards or downwards. func (p *Path) StarPolygon(n int, R, r float32, up bool) *Path { if n < 3 || Equal(R, 0.0) || Equal(r, 0.0) { return p } n *= 2 dtheta := 2.0 * math32.Pi / float32(n) theta0 := float32(0.5 * math32.Pi) if !up { theta0 += dtheta } for i := 0; i < n; i++ { theta := theta0 + float32(i)*dtheta sintheta, costheta := math32.Sincos(theta) if i == 0 { p.MoveTo(R*costheta, R*sintheta) } else if i%2 == 0 { p.LineTo(R*costheta, R*sintheta) } else { p.LineTo(r*costheta, r*sintheta) } } p.Close() return p } // Grid adds a stroked grid of width w and height h, // with grid line thickness r, and the number of cells horizontally // and vertically as nx and ny respectively. func (p *Path) Grid(w, h float32, nx, ny int, r float32) *Path { if nx < 1 || ny < 1 || w <= float32(nx+1)*r || h <= float32(ny+1)*r { return p } p.Rectangle(0, 0, w, h) dx, dy := (w-float32(nx+1)*r)/float32(nx), (h-float32(ny+1)*r)/float32(ny) cell := New().Rectangle(0, 0, dx, dy).Reverse() for j := 0; j < ny; j++ { for i := 0; i < nx; i++ { x := r + float32(i)*(r+dx) y := r + float32(j)*(r+dy) *p = p.Append(cell.Translate(x, y)) } } return p } func ArcToQuad(start math32.Vector2, rx, ry, phi float32, large, sweep bool, end math32.Vector2) Path { p := Path{} p.MoveTo(start.X, start.Y) for _, bezier := range ellipseToQuadraticBeziers(start, rx, ry, phi, large, sweep, end) { p.QuadTo(bezier[1].X, bezier[1].Y, bezier[2].X, bezier[2].Y) } return p } func ArcToCube(start math32.Vector2, rx, ry, phi float32, large, sweep bool, end math32.Vector2) Path { p := Path{} p.MoveTo(start.X, start.Y) for _, bezier := range ellipseToCubicBeziers(start, rx, ry, phi, large, sweep, end) { p.CubeTo(bezier[1].X, bezier[1].Y, bezier[2].X, bezier[2].Y, bezier[3].X, bezier[3].Y) } return p } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import "cogentcore.org/core/math32" //go:generate core generate // FillRules specifies the algorithm for which area is to be filled and which not, // in particular when multiple subpaths overlap. The NonZero rule is the default // and will fill any point that is being enclosed by an unequal number of paths // winding clock-wise and counter clock-wise, otherwise it will not be filled. // The EvenOdd rule will fill any point that is being enclosed by an uneven number // of paths, whichever their direction. Positive fills only counter clock-wise // oriented paths, while Negative fills only clock-wise oriented paths. type FillRules int32 //enums:enum -transform lower const ( NonZero FillRules = iota EvenOdd Positive Negative ) func (fr FillRules) Fills(windings int) bool { switch fr { case NonZero: return windings != 0 case EvenOdd: return windings%2 != 0 case Positive: return 0 < windings case Negative: return windings < 0 } return false } // todo: these need serious work: // 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 ) // Caps specifies the end-cap of a stroked line: stroke-linecap property in SVG type Caps int32 //enums:enum -trim-prefix Cap -transform kebab const ( // CapButt indicates to draw no line caps; it draws a // line with the length of the specified length. CapButt Caps = iota // CapRound indicates to draw a semicircle on each line // end with a diameter of the stroke width. CapRound // CapSquare 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. CapSquare ) // Joins specifies the way stroked lines are joined together: // stroke-linejoin property in SVG type Joins int32 //enums:enum -trim-prefix Join -transform kebab const ( JoinMiter Joins = iota JoinMiterClip JoinRound JoinBevel JoinArcs JoinArcsClip ) // Dash patterns var ( Solid = []float32{} Dotted = []float32{1.0, 2.0} DenselyDotted = []float32{1.0, 1.0} SparselyDotted = []float32{1.0, 4.0} Dashed = []float32{3.0, 3.0} DenselyDashed = []float32{3.0, 1.0} SparselyDashed = []float32{3.0, 6.0} Dashdotted = []float32{3.0, 2.0, 1.0, 2.0} DenselyDashdotted = []float32{3.0, 1.0, 1.0, 1.0} SparselyDashdotted = []float32{3.0, 4.0, 1.0, 4.0} ) func ScaleDash(scale float32, offset float32, d []float32) (float32, []float32) { d2 := make([]float32, len(d)) for i := range d { d2[i] = d[i] * scale } return offset * scale, d2 } // DirectionIndex returns the direction of the path at the given index // into Path and t in [0.0,1.0]. Path must not contain subpaths, // and will return the path's starting direction when i points // to a MoveTo, or the path's final direction when i points to // a Close of zero-length. func DirectionIndex(p Path, i int, t float32) math32.Vector2 { last := len(p) if p[last-1] == Close && EqualPoint(math32.Vec2(p[last-CmdLen(Close)-3], p[last-CmdLen(Close)-2]), math32.Vec2(p[last-3], p[last-2])) { // point-closed last -= CmdLen(Close) } if i == 0 { // get path's starting direction when i points to MoveTo i = 4 t = 0.0 } else if i < len(p) && i == last { // get path's final direction when i points to zero-length Close i -= CmdLen(p[i-1]) t = 1.0 } if i < 0 || len(p) <= i || last < i+CmdLen(p[i]) { return math32.Vector2{} } cmd := p[i] var start math32.Vector2 if i == 0 { start = math32.Vec2(p[last-3], p[last-2]) } else { start = math32.Vec2(p[i-3], p[i-2]) } i += CmdLen(cmd) end := math32.Vec2(p[i-3], p[i-2]) switch cmd { case LineTo, Close: return end.Sub(start).Normal() case QuadTo: cp := math32.Vec2(p[i-5], p[i-4]) return QuadraticBezierDeriv(start, cp, end, t).Normal() case CubeTo: cp1 := math32.Vec2(p[i-7], p[i-6]) cp2 := math32.Vec2(p[i-5], p[i-4]) return CubicBezierDeriv(start, cp1, cp2, end, t).Normal() case ArcTo: rx, ry, phi := p[i-7], p[i-6], p[i-5] large, sweep := ToArcFlags(p[i-4]) _, _, theta0, theta1 := EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) theta := theta0 + t*(theta1-theta0) return EllipseDeriv(rx, ry, phi, sweep, theta).Normal() } return math32.Vector2{} } // Direction returns the direction of the path at the given // segment and t in [0.0,1.0] along that path. // The direction is a vector of unit length. func (p Path) Direction(seg int, t float32) math32.Vector2 { if len(p) <= 4 { return math32.Vector2{} } curSeg := 0 iStart, iSeg, iEnd := 0, 0, 0 for i := 0; i < len(p); { cmd := p[i] if cmd == MoveTo { if seg < curSeg { pi := p[iStart:iEnd] return DirectionIndex(pi, iSeg-iStart, t) } iStart = i } if seg == curSeg { iSeg = i } i += CmdLen(cmd) } return math32.Vector2{} // if segment doesn't exist } // CoordDirections returns the direction of the segment start/end points. // It will return the average direction at the intersection of two // end points, and for an open path it will simply return the direction // of the start and end points of the path. func (p Path) CoordDirections() []math32.Vector2 { if len(p) <= 4 { return []math32.Vector2{{}} } last := len(p) if p[last-1] == Close && EqualPoint(math32.Vec2(p[last-CmdLen(Close)-3], p[last-CmdLen(Close)-2]), math32.Vec2(p[last-3], p[last-2])) { // point-closed last -= CmdLen(Close) } dirs := []math32.Vector2{} var closed bool var dirPrev math32.Vector2 for i := 4; i < last; { cmd := p[i] dir := DirectionIndex(p, i, 0.0) if i == 0 { dirs = append(dirs, dir) } else { dirs = append(dirs, dirPrev.Add(dir).Normal()) } dirPrev = DirectionIndex(p, i, 1.0) closed = cmd == Close i += CmdLen(cmd) } if closed { dirs[0] = dirs[0].Add(dirPrev).Normal() dirs = append(dirs, dirs[0]) } else { dirs = append(dirs, dirPrev) } return dirs } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package stroke import ( "cogentcore.org/core/paint/ppath" "cogentcore.org/core/paint/ppath/intersect" ) // Dash returns a new path that consists of dashes. // The elements in d specify the width of the dashes and gaps. // It will alternate between dashes and gaps when picking widths. // If d is an array of odd length, it is equivalent of passing d // twice in sequence. The offset specifies the offset used into d // (or negative offset into the path). // Dash will be applied to each subpath independently. func Dash(p ppath.Path, offset float32, d ...float32) ppath.Path { offset, d = dashCanonical(offset, d) if len(d) == 0 { return p } else if len(d) == 1 && d[0] == 0.0 { return ppath.Path{} } if len(d)%2 == 1 { // if d is uneven length, dash and space lengths alternate. Duplicate d so that uneven indices are always spaces d = append(d, d...) } i0, pos0 := dashStart(offset, d) q := ppath.Path{} for _, ps := range p.Split() { i := i0 pos := pos0 t := []float32{} length := intersect.Length(ps) for pos+d[i]+ppath.Epsilon < length { pos += d[i] if 0.0 < pos { t = append(t, pos) } i++ if i == len(d) { i = 0 } } j0 := 0 endsInDash := i%2 == 0 if len(t)%2 == 1 && endsInDash || len(t)%2 == 0 && !endsInDash { j0 = 1 } qd := ppath.Path{} pd := intersect.SplitAt(ps, t...) for j := j0; j < len(pd)-1; j += 2 { qd = qd.Append(pd[j]) } if endsInDash { if ps.Closed() { qd = pd[len(pd)-1].Join(qd) } else { qd = qd.Append(pd[len(pd)-1]) } } q = q.Append(qd) } return q } func dashStart(offset float32, d []float32) (int, float32) { i0 := 0 // index in d for d[i0] <= offset { offset -= d[i0] i0++ if i0 == len(d) { i0 = 0 } } pos0 := -offset // negative if offset is halfway into dash if offset < 0.0 { dTotal := float32(0.0) for _, dd := range d { dTotal += dd } pos0 = -(dTotal + offset) // handle negative offsets } return i0, pos0 } // dashCanonical returns an optimized dash array. func dashCanonical(offset float32, d []float32) (float32, []float32) { if len(d) == 0 { return 0.0, []float32{} } // remove zeros except first and last for i := 1; i < len(d)-1; i++ { if ppath.Equal(d[i], 0.0) { d[i-1] += d[i+1] d = append(d[:i], d[i+2:]...) i-- } } // remove first zero, collapse with second and last if ppath.Equal(d[0], 0.0) { if len(d) < 3 { return 0.0, []float32{0.0} } offset -= d[1] d[len(d)-1] += d[1] d = d[2:] } // remove last zero, collapse with fist and second to last if ppath.Equal(d[len(d)-1], 0.0) { if len(d) < 3 { return 0.0, []float32{} } offset += d[len(d)-2] d[0] += d[len(d)-2] d = d[:len(d)-2] } // if there are zeros or negatives, don't draw any dashes for i := 0; i < len(d); i++ { if d[i] < 0.0 || ppath.Equal(d[i], 0.0) { return 0.0, []float32{0.0} } } // remove repeated patterns REPEAT: for len(d)%2 == 0 { mid := len(d) / 2 for i := 0; i < mid; i++ { if !ppath.Equal(d[i], d[mid+i]) { break REPEAT } } d = d[:mid] } return offset, d } func checkDash(p ppath.Path, offset float32, d []float32) ([]float32, bool) { offset, d = dashCanonical(offset, d) if len(d) == 0 { return d, true // stroke without dashes } else if len(d) == 1 && d[0] == 0.0 { return d[:0], false // no dashes, no stroke } length := intersect.Length(p) i, pos := dashStart(offset, d) if length <= d[i]-pos { if i%2 == 0 { return d[:0], true // first dash covers whole path, stroke without dashes } return d[:0], false // first space covers whole path, no stroke } return d, true } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package stroke //go:generate core generate import ( "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" "cogentcore.org/core/paint/ppath/intersect" ) // ellipseNormal returns the normal to the right at angle theta of the ellipse, given rotation phi. func ellipseNormal(rx, ry, phi float32, sweep bool, theta, d float32) math32.Vector2 { return ppath.EllipseDeriv(rx, ry, phi, sweep, theta).Rot90CW().Normal().MulScalar(d) } // NOTE: implementation inspired from github.com/golang/freetype/raster/stroke.go // Stroke converts a path into a stroke of width w and returns a new path. // It uses cr to cap the start and end of the path, and jr to join all path elements. // If the path closes itself, it will use a join between the start and end instead // of capping them. The tolerance is the maximum deviation from the original path // when flattening Béziers and optimizing the stroke. func Stroke(p ppath.Path, w float32, cr Capper, jr Joiner, tolerance float32) ppath.Path { if cr == nil { cr = ButtCap } if jr == nil { jr = MiterJoin } q := ppath.Path{} halfWidth := math32.Abs(w) / 2.0 for _, pi := range p.Split() { rhs, lhs := offset(pi, halfWidth, cr, jr, true, tolerance) if rhs == nil { continue } else if lhs == nil { // open path q = q.Append(intersect.Settle(rhs, ppath.Positive)) } else { // closed path // inner path should go opposite direction to cancel the outer path if intersect.CCW(pi) { q = q.Append(intersect.Settle(rhs, ppath.Positive)) q = q.Append(intersect.Settle(lhs, ppath.Positive).Reverse()) } else { // outer first, then inner q = q.Append(intersect.Settle(lhs, ppath.Negative)) q = q.Append(intersect.Settle(rhs, ppath.Negative).Reverse()) } } } return q } func CapFromStyle(st ppath.Caps) Capper { switch st { case ppath.CapButt: return ButtCap case ppath.CapRound: return RoundCap case ppath.CapSquare: return SquareCap } return ButtCap } func JoinFromStyle(st ppath.Joins) Joiner { switch st { case ppath.JoinMiter: return MiterJoin case ppath.JoinMiterClip: return MiterClipJoin case ppath.JoinRound: return RoundJoin case ppath.JoinBevel: return BevelJoin case ppath.JoinArcs: return ArcsJoin case ppath.JoinArcsClip: return ArcsClipJoin } return MiterJoin } // Capper implements Cap, with rhs the path to append to, // halfWidth the half width of the stroke, pivot the pivot point around // which to construct a cap, and n0 the normal at the start of the path. // The length of n0 is equal to the halfWidth. type Capper interface { Cap(*ppath.Path, float32, math32.Vector2, math32.Vector2) } // RoundCap caps the start or end of a path by a round cap. var RoundCap Capper = RoundCapper{} // RoundCapper is a round capper. type RoundCapper struct{} // Cap adds a cap to path p of width 2*halfWidth, // at a pivot point and initial normal direction of n0. func (RoundCapper) Cap(p *ppath.Path, halfWidth float32, pivot, n0 math32.Vector2) { end := pivot.Sub(n0) p.ArcTo(halfWidth, halfWidth, 0, false, true, end.X, end.Y) } func (RoundCapper) String() string { return "Round" } // ButtCap caps the start or end of a path by a butt cap. var ButtCap Capper = ButtCapper{} // ButtCapper is a butt capper. type ButtCapper struct{} // Cap adds a cap to path p of width 2*halfWidth, // at a pivot point and initial normal direction of n0. func (ButtCapper) Cap(p *ppath.Path, halfWidth float32, pivot, n0 math32.Vector2) { end := pivot.Sub(n0) p.LineTo(end.X, end.Y) } func (ButtCapper) String() string { return "Butt" } // SquareCap caps the start or end of a path by a square cap. var SquareCap Capper = SquareCapper{} // SquareCapper is a square capper. type SquareCapper struct{} // Cap adds a cap to path p of width 2*halfWidth, // at a pivot point and initial normal direction of n0. func (SquareCapper) Cap(p *ppath.Path, halfWidth float32, pivot, n0 math32.Vector2) { e := n0.Rot90CCW() corner1 := pivot.Add(e).Add(n0) corner2 := pivot.Add(e).Sub(n0) end := pivot.Sub(n0) p.LineTo(corner1.X, corner1.Y) p.LineTo(corner2.X, corner2.Y) p.LineTo(end.X, end.Y) } func (SquareCapper) String() string { return "Square" } //////// // Joiner implements Join, with rhs the right path and lhs the left path // to append to, pivot the intersection of both path elements, n0 and n1 // the normals at the start and end of the path respectively. // The length of n0 and n1 are equal to the halfWidth. type Joiner interface { Join(*ppath.Path, *ppath.Path, float32, math32.Vector2, math32.Vector2, math32.Vector2, float32, float32) } // BevelJoin connects two path elements by a linear join. var BevelJoin Joiner = BevelJoiner{} // BevelJoiner is a bevel joiner. type BevelJoiner struct{} // Join adds a join to a right-hand-side and left-hand-side path, // of width 2*halfWidth, around a pivot point with starting and // ending normals of n0 and n1, and radius of curvatures of the // previous and next segments. func (BevelJoiner) Join(rhs, lhs *ppath.Path, halfWidth float32, pivot, n0, n1 math32.Vector2, r0, r1 float32) { rEnd := pivot.Add(n1) lEnd := pivot.Sub(n1) rhs.LineTo(rEnd.X, rEnd.Y) lhs.LineTo(lEnd.X, lEnd.Y) } func (BevelJoiner) String() string { return "Bevel" } // RoundJoin connects two path elements by a round join. var RoundJoin Joiner = RoundJoiner{} // RoundJoiner is a round joiner. type RoundJoiner struct{} func (RoundJoiner) Join(rhs, lhs *ppath.Path, halfWidth float32, pivot, n0, n1 math32.Vector2, r0, r1 float32) { rEnd := pivot.Add(n1) lEnd := pivot.Sub(n1) cw := 0.0 <= n0.Rot90CW().Dot(n1) if cw { // bend to the right, ie. CW (or 180 degree turn) rhs.LineTo(rEnd.X, rEnd.Y) lhs.ArcTo(halfWidth, halfWidth, 0.0, false, false, lEnd.X, lEnd.Y) } else { // bend to the left, ie. CCW rhs.ArcTo(halfWidth, halfWidth, 0.0, false, true, rEnd.X, rEnd.Y) lhs.LineTo(lEnd.X, lEnd.Y) } } func (RoundJoiner) String() string { return "Round" } // MiterJoin connects two path elements by extending the ends // of the paths as lines until they meet. // If this point is further than the limit, this will result in a bevel // join (MiterJoin) or they will meet at the limit (MiterClipJoin). var MiterJoin Joiner = MiterJoiner{BevelJoin, 4.0} var MiterClipJoin Joiner = MiterJoiner{nil, 4.0} // TODO: should extend limit*halfwidth before bevel // MiterJoiner is a miter joiner. type MiterJoiner struct { GapJoiner Joiner Limit float32 } func (j MiterJoiner) Join(rhs, lhs *ppath.Path, halfWidth float32, pivot, n0, n1 math32.Vector2, r0, r1 float32) { if ppath.EqualPoint(n0, n1.Negate()) { BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1) return } cw := 0.0 <= n0.Rot90CW().Dot(n1) hw := halfWidth if cw { hw = -hw // used to calculate |R|, when running CW then n0 and n1 point the other way, so the sign of r0 and r1 is negated } // note that cos(theta) below refers to sin(theta/2) in the documentation of stroke-miterlimit // in https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit theta := ppath.AngleBetween(n0, n1) / 2.0 // half the angle between normals d := hw / math32.Cos(theta) // half the miter length limit := math32.Max(j.Limit, 1.001) // otherwise nearly linear joins will also get clipped clip := !math32.IsNaN(limit) && limit*halfWidth < math32.Abs(d) if clip && j.GapJoiner != nil { j.GapJoiner.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1) return } rEnd := pivot.Add(n1) lEnd := pivot.Sub(n1) mid := pivot.Add(n0.Add(n1).Normal().MulScalar(d)) if clip { // miter-clip t := math32.Abs(limit * halfWidth / d) if cw { // bend to the right, ie. CW mid0 := lhs.Pos().Lerp(mid, t) mid1 := lEnd.Lerp(mid, t) lhs.LineTo(mid0.X, mid0.Y) lhs.LineTo(mid1.X, mid1.Y) } else { mid0 := rhs.Pos().Lerp(mid, t) mid1 := rEnd.Lerp(mid, t) rhs.LineTo(mid0.X, mid0.Y) rhs.LineTo(mid1.X, mid1.Y) } } else { if cw { // bend to the right, ie. CW lhs.LineTo(mid.X, mid.Y) } else { rhs.LineTo(mid.X, mid.Y) } } rhs.LineTo(rEnd.X, rEnd.Y) lhs.LineTo(lEnd.X, lEnd.Y) } func (j MiterJoiner) String() string { if j.GapJoiner == nil { return "MiterClip" } return "Miter" } // ArcsJoin connects two path elements by extending the ends // of the paths as circle arcs until they meet. // If this point is further than the limit, this will result // in a bevel join (ArcsJoin) or they will meet at the limit (ArcsClipJoin). var ArcsJoin Joiner = ArcsJoiner{BevelJoin, 4.0} var ArcsClipJoin Joiner = ArcsJoiner{nil, 4.0} // ArcsJoiner is an arcs joiner. type ArcsJoiner struct { GapJoiner Joiner Limit float32 } func closestArcIntersection(c math32.Vector2, cw bool, pivot, i0, i1 math32.Vector2) math32.Vector2 { thetaPivot := ppath.Angle(pivot.Sub(c)) dtheta0 := ppath.Angle(i0.Sub(c)) - thetaPivot dtheta1 := ppath.Angle(i1.Sub(c)) - thetaPivot if cw { // arc runs clockwise, so look the other way around dtheta0 = -dtheta0 dtheta1 = -dtheta1 } if ppath.AngleNorm(dtheta1) < ppath.AngleNorm(dtheta0) { return i1 } return i0 } func (j ArcsJoiner) Join(rhs, lhs *ppath.Path, halfWidth float32, pivot, n0, n1 math32.Vector2, r0, r1 float32) { if ppath.EqualPoint(n0, n1.Negate()) { BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1) return } else if math32.IsNaN(r0) && math32.IsNaN(r1) { MiterJoiner(j).Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1) return } limit := math32.Max(j.Limit, 1.001) // 1.001 so that nearly linear joins will not get clipped cw := 0.0 <= n0.Rot90CW().Dot(n1) hw := halfWidth if cw { hw = -hw // used to calculate |R|, when running CW then n0 and n1 point the other way, so the sign of r0 and r1 is negated } // r is the radius of the original curve, R the radius of the stroke curve, c are the centers of the circles c0 := pivot.Add(n0.Normal().MulScalar(-r0)) c1 := pivot.Add(n1.Normal().MulScalar(-r1)) R0, R1 := math32.Abs(r0+hw), math32.Abs(r1+hw) // TODO: can simplify if intersection returns angles too? var i0, i1 math32.Vector2 var ok bool if math32.IsNaN(r0) { line := pivot.Add(n0) if cw { line = pivot.Sub(n0) } i0, i1, ok = intersect.IntersectionRayCircle(line, line.Add(n0.Rot90CCW()), c1, R1) } else if math32.IsNaN(r1) { line := pivot.Add(n1) if cw { line = pivot.Sub(n1) } i0, i1, ok = intersect.IntersectionRayCircle(line, line.Add(n1.Rot90CCW()), c0, R0) } else { i0, i1, ok = intersect.IntersectionCircleCircle(c0, R0, c1, R1) } if !ok { // no intersection BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1) return } // find the closest intersection when following the arc (using either arc r0 or r1 with center c0 or c1 respectively) var mid math32.Vector2 if !math32.IsNaN(r0) { mid = closestArcIntersection(c0, r0 < 0.0, pivot, i0, i1) } else { mid = closestArcIntersection(c1, 0.0 <= r1, pivot, i0, i1) } // check arc limit d := mid.Sub(pivot).Length() clip := !math32.IsNaN(limit) && limit*halfWidth < d if clip && j.GapJoiner != nil { j.GapJoiner.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1) return } mid2 := mid if clip { // arcs-clip start, end := pivot.Add(n0), pivot.Add(n1) if cw { start, end = pivot.Sub(n0), pivot.Sub(n1) } var clipMid, clipNormal math32.Vector2 if !math32.IsNaN(r0) && !math32.IsNaN(r1) && (0.0 < r0) == (0.0 < r1) { // circle have opposite direction/sweep // NOTE: this may cause the bevel to be imperfectly oriented clipMid = mid.Sub(pivot).Normal().MulScalar(limit * halfWidth) clipNormal = clipMid.Rot90CCW() } else { // circle in between both stroke edges rMid := (r0 - r1) / 2.0 if math32.IsNaN(r0) { rMid = -(r1 + hw) * 2.0 } else if math32.IsNaN(r1) { rMid = (r0 + hw) * 2.0 } sweep := 0.0 < rMid RMid := math32.Abs(rMid) cx, cy, a0, _ := ppath.EllipseToCenter(pivot.X, pivot.Y, RMid, RMid, 0.0, false, sweep, mid.X, mid.Y) cMid := math32.Vector2{cx, cy} dtheta := limit * halfWidth / rMid clipMid = ppath.EllipsePos(RMid, RMid, 0.0, cMid.X, cMid.Y, a0+dtheta) clipNormal = ellipseNormal(RMid, RMid, 0.0, sweep, a0+dtheta, 1.0) } if math32.IsNaN(r1) { i0, ok = intersect.IntersectionRayLine(clipMid, clipMid.Add(clipNormal), mid, end) if !ok { // not sure when this occurs BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1) return } mid2 = i0 } else { i0, i1, ok = intersect.IntersectionRayCircle(clipMid, clipMid.Add(clipNormal), c1, R1) if !ok { // not sure when this occurs BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1) return } mid2 = closestArcIntersection(c1, 0.0 <= r1, pivot, i0, i1) } if math32.IsNaN(r0) { i0, ok = intersect.IntersectionRayLine(clipMid, clipMid.Add(clipNormal), start, mid) if !ok { // not sure when this occurs BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1) return } mid = i0 } else { i0, i1, ok = intersect.IntersectionRayCircle(clipMid, clipMid.Add(clipNormal), c0, R0) if !ok { // not sure when this occurs BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1) return } mid = closestArcIntersection(c0, r0 < 0.0, pivot, i0, i1) } } rEnd := pivot.Add(n1) lEnd := pivot.Sub(n1) if cw { // bend to the right, ie. CW rhs.LineTo(rEnd.X, rEnd.Y) if math32.IsNaN(r0) { lhs.LineTo(mid.X, mid.Y) } else { lhs.ArcTo(R0, R0, 0.0, false, 0.0 < r0, mid.X, mid.Y) } if clip { lhs.LineTo(mid2.X, mid2.Y) } if math32.IsNaN(r1) { lhs.LineTo(lEnd.X, lEnd.Y) } else { lhs.ArcTo(R1, R1, 0.0, false, 0.0 < r1, lEnd.X, lEnd.Y) } } else { // bend to the left, ie. CCW if math32.IsNaN(r0) { rhs.LineTo(mid.X, mid.Y) } else { rhs.ArcTo(R0, R0, 0.0, false, 0.0 < r0, mid.X, mid.Y) } if clip { rhs.LineTo(mid2.X, mid2.Y) } if math32.IsNaN(r1) { rhs.LineTo(rEnd.X, rEnd.Y) } else { rhs.ArcTo(R1, R1, 0.0, false, 0.0 < r1, rEnd.X, rEnd.Y) } lhs.LineTo(lEnd.X, lEnd.Y) } } func (j ArcsJoiner) String() string { if j.GapJoiner == nil { return "ArcsClip" } return "Arcs" } // optimizeClose removes a superfluous first line segment in-place // of a subpath. If both the first and last segment are line segments // and are colinear, move the start of the path forward one segment func optimizeClose(p *ppath.Path) { if len(*p) == 0 || (*p)[len(*p)-1] != ppath.Close { return } // find last MoveTo end := math32.Vector2{} iMoveTo := len(*p) for 0 < iMoveTo { cmd := (*p)[iMoveTo-1] iMoveTo -= ppath.CmdLen(cmd) if cmd == ppath.MoveTo { end = math32.Vec2((*p)[iMoveTo+1], (*p)[iMoveTo+2]) break } } if (*p)[iMoveTo] == ppath.MoveTo && (*p)[iMoveTo+ppath.CmdLen(ppath.MoveTo)] == ppath.LineTo && iMoveTo+ppath.CmdLen(ppath.MoveTo)+ppath.CmdLen(ppath.LineTo) < len(*p)-ppath.CmdLen(ppath.Close) { // replace Close + MoveTo + LineTo by Close + MoveTo if equidirectional // move Close and MoveTo forward along the path start := math32.Vec2((*p)[len(*p)-ppath.CmdLen(ppath.Close)-3], (*p)[len(*p)-ppath.CmdLen(ppath.Close)-2]) nextEnd := math32.Vec2((*p)[iMoveTo+ppath.CmdLen(ppath.MoveTo)+ppath.CmdLen(ppath.LineTo)-3], (*p)[iMoveTo+ppath.CmdLen(ppath.MoveTo)+ppath.CmdLen(ppath.LineTo)-2]) if ppath.Equal(ppath.AngleBetween(end.Sub(start), nextEnd.Sub(end)), 0.0) { // update Close (*p)[len(*p)-3] = nextEnd.X (*p)[len(*p)-2] = nextEnd.Y // update MoveTo (*p)[iMoveTo+1] = nextEnd.X (*p)[iMoveTo+2] = nextEnd.Y // remove LineTo *p = append((*p)[:iMoveTo+ppath.CmdLen(ppath.MoveTo)], (*p)[iMoveTo+ppath.CmdLen(ppath.MoveTo)+ppath.CmdLen(ppath.LineTo):]...) } } } func optimizeInnerBend(p ppath.Path, i int) { // i is the index of the line segment in the inner bend connecting both edges ai := i - ppath.CmdLen(p[i-1]) if ai == 0 { return } if i >= len(p) { return } bi := i + ppath.CmdLen(p[i]) a0 := math32.Vector2{p[ai-3], p[ai-2]} b0 := math32.Vector2{p[bi-3], p[bi-2]} if bi == len(p) { // inner bend is at the path's start bi = 4 } // TODO: implement other segment combinations zs_ := [2]intersect.Intersection{} zs := zs_[:] if (p[ai] == ppath.LineTo || p[ai] == ppath.Close) && (p[bi] == ppath.LineTo || p[bi] == ppath.Close) { zs = intersect.IntersectionSegment(zs[:0], a0, p[ai:ai+4], b0, p[bi:bi+4]) // TODO: check conditions for pathological cases if len(zs) == 1 && zs[0].T[0] != 0.0 && zs[0].T[0] != 1.0 && zs[0].T[1] != 0.0 && zs[0].T[1] != 1.0 { p[ai+1] = zs[0].X p[ai+2] = zs[0].Y if bi == 4 { // inner bend is at the path's start if p[i] == ppath.Close { if p[ai] == ppath.LineTo { p[ai] = ppath.Close p[ai+3] = ppath.Close } else { p = append(p, ppath.Close, zs[0].X, zs[1].Y, ppath.Close) } } p = p[:i] p[1] = zs[0].X p[2] = zs[0].Y } else { p = append(p[:i], p[bi:]...) } } } } type pathStrokeState struct { cmd float32 p0, p1 math32.Vector2 // position of start and end n0, n1 math32.Vector2 // normal of start and end (points right when walking the path) r0, r1 float32 // radius of start and end cp1, cp2 math32.Vector2 // Béziers rx, ry, rot, theta0, theta1 float32 // arcs large, sweep bool // arcs } // offset returns the rhs and lhs paths from offsetting a path // (must not have subpaths). It closes rhs and lhs when p is closed as well. func offset(p ppath.Path, halfWidth float32, cr Capper, jr Joiner, strokeOpen bool, tolerance float32) (ppath.Path, ppath.Path) { // only non-empty paths are evaluated closed := false states := []pathStrokeState{} var start, end math32.Vector2 for i := 0; i < len(p); { cmd := p[i] switch cmd { case ppath.MoveTo: end = math32.Vector2{p[i+1], p[i+2]} case ppath.LineTo: end = math32.Vector2{p[i+1], p[i+2]} n := end.Sub(start).Rot90CW().Normal().MulScalar(halfWidth) states = append(states, pathStrokeState{ cmd: ppath.LineTo, p0: start, p1: end, n0: n, n1: n, r0: math32.NaN(), r1: math32.NaN(), }) case ppath.QuadTo, ppath.CubeTo: var cp1, cp2 math32.Vector2 if cmd == ppath.QuadTo { cp := math32.Vector2{p[i+1], p[i+2]} end = math32.Vector2{p[i+3], p[i+4]} cp1, cp2 = ppath.QuadraticToCubicBezier(start, cp, end) } else { cp1 = math32.Vector2{p[i+1], p[i+2]} cp2 = math32.Vector2{p[i+3], p[i+4]} end = math32.Vector2{p[i+5], p[i+6]} } n0 := intersect.CubicBezierNormal(start, cp1, cp2, end, 0.0, halfWidth) n1 := intersect.CubicBezierNormal(start, cp1, cp2, end, 1.0, halfWidth) r0 := intersect.CubicBezierCurvatureRadius(start, cp1, cp2, end, 0.0) r1 := intersect.CubicBezierCurvatureRadius(start, cp1, cp2, end, 1.0) states = append(states, pathStrokeState{ cmd: ppath.CubeTo, p0: start, p1: end, n0: n0, n1: n1, r0: r0, r1: r1, cp1: cp1, cp2: cp2, }) case ppath.ArcTo: rx, ry, phi := p[i+1], p[i+2], p[i+3] large, sweep := ppath.ToArcFlags(p[i+4]) end = math32.Vector2{p[i+5], p[i+6]} _, _, theta0, theta1 := ppath.EllipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y) n0 := ellipseNormal(rx, ry, phi, sweep, theta0, halfWidth) n1 := ellipseNormal(rx, ry, phi, sweep, theta1, halfWidth) r0 := intersect.EllipseCurvatureRadius(rx, ry, sweep, theta0) r1 := intersect.EllipseCurvatureRadius(rx, ry, sweep, theta1) states = append(states, pathStrokeState{ cmd: ppath.ArcTo, p0: start, p1: end, n0: n0, n1: n1, r0: r0, r1: r1, rx: rx, ry: ry, rot: phi * 180.0 / math32.Pi, theta0: theta0, theta1: theta1, large: large, sweep: sweep, }) case ppath.Close: end = math32.Vector2{p[i+1], p[i+2]} if !ppath.Equal(start.X, end.X) || !ppath.Equal(start.Y, end.Y) { n := end.Sub(start).Rot90CW().Normal().MulScalar(halfWidth) states = append(states, pathStrokeState{ cmd: ppath.LineTo, p0: start, p1: end, n0: n, n1: n, r0: math32.NaN(), r1: math32.NaN(), }) } closed = true } start = end i += ppath.CmdLen(cmd) } if len(states) == 0 { return nil, nil } rhs, lhs := ppath.Path{}, ppath.Path{} rStart := states[0].p0.Add(states[0].n0) lStart := states[0].p0.Sub(states[0].n0) rhs.MoveTo(rStart.X, rStart.Y) lhs.MoveTo(lStart.X, lStart.Y) rhsJoinIndex, lhsJoinIndex := -1, -1 for i, cur := range states { switch cur.cmd { case ppath.LineTo: rEnd := cur.p1.Add(cur.n1) lEnd := cur.p1.Sub(cur.n1) rhs.LineTo(rEnd.X, rEnd.Y) lhs.LineTo(lEnd.X, lEnd.Y) case ppath.CubeTo: rhs = rhs.Join(intersect.FlattenCubicBezier(cur.p0, cur.cp1, cur.cp2, cur.p1, halfWidth, tolerance)) lhs = lhs.Join(intersect.FlattenCubicBezier(cur.p0, cur.cp1, cur.cp2, cur.p1, -halfWidth, tolerance)) case ppath.ArcTo: rStart := cur.p0.Add(cur.n0) lStart := cur.p0.Sub(cur.n0) rEnd := cur.p1.Add(cur.n1) lEnd := cur.p1.Sub(cur.n1) dr := halfWidth if !cur.sweep { // bend to the right, ie. CW dr = -dr } rLambda := ppath.EllipseRadiiCorrection(rStart, cur.rx+dr, cur.ry+dr, cur.rot*math32.Pi/180.0, rEnd) lLambda := ppath.EllipseRadiiCorrection(lStart, cur.rx-dr, cur.ry-dr, cur.rot*math32.Pi/180.0, lEnd) if rLambda <= 1.0 && lLambda <= 1.0 { rLambda, lLambda = 1.0, 1.0 } rhs.ArcTo(rLambda*(cur.rx+dr), rLambda*(cur.ry+dr), cur.rot, cur.large, cur.sweep, rEnd.X, rEnd.Y) lhs.ArcTo(lLambda*(cur.rx-dr), lLambda*(cur.ry-dr), cur.rot, cur.large, cur.sweep, lEnd.X, lEnd.Y) } // optimize inner bend if 0 < i { prev := states[i-1] cw := 0.0 <= prev.n1.Rot90CW().Dot(cur.n0) if cw && rhsJoinIndex != -1 { optimizeInnerBend(rhs, rhsJoinIndex) } else if !cw && lhsJoinIndex != -1 { optimizeInnerBend(lhs, lhsJoinIndex) } } rhsJoinIndex = -1 lhsJoinIndex = -1 // join the cur and next path segments if i+1 < len(states) || closed { next := states[0] if i+1 < len(states) { next = states[i+1] } if !ppath.EqualPoint(cur.n1, next.n0) { rhsJoinIndex = len(rhs) lhsJoinIndex = len(lhs) jr.Join(&rhs, &lhs, halfWidth, cur.p1, cur.n1, next.n0, cur.r1, next.r0) } } } if closed { rhs.Close() lhs.Close() // optimize inner bend if 1 < len(states) { cw := 0.0 <= states[len(states)-1].n1.Rot90CW().Dot(states[0].n0) if cw && rhsJoinIndex != -1 { optimizeInnerBend(rhs, rhsJoinIndex) } else if !cw && lhsJoinIndex != -1 { optimizeInnerBend(lhs, lhsJoinIndex) } } optimizeClose(&rhs) optimizeClose(&lhs) } else if strokeOpen { lhs = lhs.Reverse() cr.Cap(&rhs, halfWidth, states[len(states)-1].p1, states[len(states)-1].n1) rhs = rhs.Join(lhs) cr.Cap(&rhs, halfWidth, states[0].p0, states[0].n0.Negate()) lhs = nil rhs.Close() optimizeClose(&rhs) } return rhs, lhs } // Offset offsets the path by w and returns a new path. // A positive w will offset the path to the right-hand side, that is, // it expands CCW oriented contours and contracts CW oriented contours. // If you don't know the orientation you can use `Path.CCW` to find out, // but if there may be self-intersection you should use `Path.Settle` // to remove them and orient all filling contours CCW. // The tolerance is the maximum deviation from the actual offset when // flattening Béziers and optimizing the path. func Offset(p ppath.Path, w float32, tolerance float32) ppath.Path { if ppath.Equal(w, 0.0) { return p } positive := 0.0 < w w = math32.Abs(w) q := ppath.Path{} for _, pi := range p.Split() { r := ppath.Path{} rhs, lhs := offset(pi, w, ButtCap, RoundJoin, false, tolerance) if rhs == nil { continue } else if positive { r = rhs } else { r = lhs } if pi.Closed() { if intersect.CCW(pi) { r = intersect.Settle(r, ppath.Positive) } else { r = intersect.Settle(r, ppath.Negative).Reverse() } } q = q.Append(r) } return q } // Markers returns an array of start, mid and end marker paths along // the path at the coordinates between commands. // Align will align the markers with the path direction so that // the markers orient towards the path's left. func Markers(p ppath.Path, first, mid, last ppath.Path, align bool) []ppath.Path { markers := []ppath.Path{} coordPos := p.Coords() coordDir := p.CoordDirections() for i := range coordPos { q := mid if i == 0 { q = first } else if i == len(coordPos)-1 { q = last } if q != nil { pos, dir := coordPos[i], coordDir[i] m := math32.Identity2().Translate(pos.X, pos.Y) if align { m = m.Rotate(ppath.Angle(dir)) } markers = append(markers, q.Clone().Transform(m)) } } return markers } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is adapted from https://github.com/tdewolff/canvas // Copyright (c) 2015 Taco de Wolff, under an MIT License. package ppath import ( "cogentcore.org/core/math32" ) // Transform transforms the path by the given transformation matrix // and returns a new path. It modifies the path in-place. func (p Path) Transform(m math32.Matrix2) Path { xscale, yscale := m.ExtractScale() for i := 0; i < len(p); { cmd := p[i] switch cmd { case MoveTo, LineTo, Close: end := m.MulVector2AsPoint(math32.Vec2(p[i+1], p[i+2])) p[i+1] = end.X p[i+2] = end.Y case QuadTo: cp := m.MulVector2AsPoint(math32.Vec2(p[i+1], p[i+2])) end := m.MulVector2AsPoint(math32.Vec2(p[i+3], p[i+4])) p[i+1] = cp.X p[i+2] = cp.Y p[i+3] = end.X p[i+4] = end.Y case CubeTo: cp1 := m.MulVector2AsPoint(math32.Vec2(p[i+1], p[i+2])) cp2 := m.MulVector2AsPoint(math32.Vec2(p[i+3], p[i+4])) end := m.MulVector2AsPoint(math32.Vec2(p[i+5], p[i+6])) p[i+1] = cp1.X p[i+2] = cp1.Y p[i+3] = cp2.X p[i+4] = cp2.Y p[i+5] = end.X p[i+6] = end.Y case ArcTo: rx, ry, phi, large, sweep, end := p.ArcToPoints(i) // For ellipses written as the conic section equation in matrix form, we have: // [x, y] E [x; y] = 0, with E = [1/rx^2, 0; 0, 1/ry^2] // For our transformed ellipse we have [x', y'] = T [x, y], with T the affine // transformation matrix so that // (T^-1 [x'; y'])^T E (T^-1 [x'; y'] = 0 => [x', y'] T^(-T) E T^(-1) [x'; y'] = 0 // We define Q = T^(-1,T) E T^(-1) the new ellipse equation which is typically rotated // from the x-axis. That's why we find the eigenvalues and eigenvectors (the new // direction and length of the major and minor axes). T := m.Rotate(phi) invT := T.Inverse() Q := math32.Identity2().Scale(1.0/rx/rx, 1.0/ry/ry) Q = invT.Transpose().Mul(Q).Mul(invT) lambda1, lambda2, v1, v2 := Q.Eigen() rx = 1 / math32.Sqrt(lambda1) ry = 1 / math32.Sqrt(lambda2) phi = Angle(v1) if rx < ry { rx, ry = ry, rx phi = Angle(v2) } phi = AngleNorm(phi) if math32.Pi <= phi { // phi is canonical within 0 <= phi < 180 phi -= math32.Pi } if xscale*yscale < 0.0 { // flip x or y axis needs flipping of the sweep sweep = !sweep } end = m.MulVector2AsPoint(end) p[i+1] = rx p[i+2] = ry p[i+3] = phi p[i+4] = fromArcFlags(large, sweep) p[i+5] = end.X p[i+6] = end.Y } i += CmdLen(cmd) } return p } // Translate translates the path by (x,y) and returns a new path. func (p Path) Translate(x, y float32) Path { return p.Transform(math32.Identity2().Translate(x, y)) } // Scale scales the path by (x,y) and returns a new path. func (p Path) Scale(x, y float32) Path { return p.Transform(math32.Identity2().Scale(x, y)) } // ReplaceArcs replaces ArcTo commands by CubeTo commands and returns a new path. func (p *Path) ReplaceArcs() Path { return p.Replace(nil, nil, nil, ArcToCube) } // Replace replaces path segments by their respective functions, // each returning the path that will replace the segment or nil // if no replacement is to be performed. The line function will // take the start and end points. The bezier function will take // the start point, control point 1 and 2, and the end point // (i.e. a cubic Bézier, quadratic Béziers will be implicitly // converted to cubic ones). The arc function will take a start point, // the major and minor radii, the radial rotaton counter clockwise, // the large and sweep booleans, and the end point. // The replacing path will replace the path segment without any checks, // you need to make sure the be moved so that its start point connects // with the last end point of the base path before the replacement. // If the end point of the replacing path is different that the end point // of what is replaced, the path that follows will be displaced. func (p Path) Replace( line func(math32.Vector2, math32.Vector2) Path, quad func(math32.Vector2, math32.Vector2, math32.Vector2) Path, cube func(math32.Vector2, math32.Vector2, math32.Vector2, math32.Vector2) Path, arc func(math32.Vector2, float32, float32, float32, bool, bool, math32.Vector2) Path, ) Path { copied := false var start, end, cp1, cp2 math32.Vector2 for i := 0; i < len(p); { var q Path cmd := p[i] switch cmd { case LineTo, Close: if line != nil { end = p.EndPoint(i) q = line(start, end) if cmd == Close { q.Close() } } case QuadTo: if quad != nil { cp1, end = p.QuadToPoints(i) q = quad(start, cp1, end) } case CubeTo: if cube != nil { cp1, cp2, end = p.CubeToPoints(i) q = cube(start, cp1, cp2, end) } case ArcTo: if arc != nil { var rx, ry, phi float32 var large, sweep bool rx, ry, phi, large, sweep, end = p.ArcToPoints(i) q = arc(start, rx, ry, phi, large, sweep, end) } } if q != nil { if !copied { p = p.Clone() copied = true } r := append(Path{MoveTo, end.X, end.Y, MoveTo}, p[i+CmdLen(cmd):]...) p = p[: i : i+CmdLen(cmd)] // make sure not to overwrite the rest of the path p = p.Join(q) if cmd != Close { p.LineTo(end.X, end.Y) } i = len(p) p = p.Join(r) // join the rest of the base path } else { i += CmdLen(cmd) } start = math32.Vec2(p[i-3], p[i-2]) } return p } // Split splits the path into its independent subpaths. // The path is split before each MoveTo command. func (p Path) Split() []Path { if p == nil { return nil } var i, j int ps := []Path{} for j < len(p) { cmd := p[j] if i < j && cmd == MoveTo { ps = append(ps, p[i:j:j]) i = j } j += CmdLen(cmd) } if i+CmdLen(MoveTo) < j { ps = append(ps, p[i:j:j]) } return ps } // Reverse returns a new path that is the same path as p but in the reverse direction. func (p Path) Reverse() Path { if len(p) == 0 { return p } end := math32.Vector2{p[len(p)-3], p[len(p)-2]} q := make(Path, 0, len(p)) q = append(q, MoveTo, end.X, end.Y, MoveTo) closed := false first, start := end, end for i := len(p); 0 < i; { cmd := p[i-1] i -= CmdLen(cmd) end = math32.Vector2{} if 0 < i { end = math32.Vector2{p[i-3], p[i-2]} } switch cmd { case MoveTo: if closed { q = append(q, Close, first.X, first.Y, Close) closed = false } if i != 0 { q = append(q, MoveTo, end.X, end.Y, MoveTo) first = end } case Close: if !EqualPoint(start, end) { q = append(q, LineTo, end.X, end.Y, LineTo) } closed = true case LineTo: if closed && (i == 0 || p[i-1] == MoveTo) { q = append(q, Close, first.X, first.Y, Close) closed = false } else { q = append(q, LineTo, end.X, end.Y, LineTo) } case QuadTo: cx, cy := p[i+1], p[i+2] q = append(q, QuadTo, cx, cy, end.X, end.Y, QuadTo) case CubeTo: cx1, cy1 := p[i+1], p[i+2] cx2, cy2 := p[i+3], p[i+4] q = append(q, CubeTo, cx2, cy2, cx1, cy1, end.X, end.Y, CubeTo) case ArcTo: rx, ry, phi, large, sweep, _ := p.ArcToPoints(i) q = append(q, ArcTo, rx, ry, phi, fromArcFlags(large, !sweep), end.X, end.Y, ArcTo) } start = end } if closed { q = append(q, Close, first.X, first.Y, Close) } return q } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package render import ( "image" "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" "cogentcore.org/core/styles" "cogentcore.org/core/styles/sides" ) // Bounds represents an optimized rounded rectangle form of clipping, // which is critical for GUI rendering. type Bounds struct { // Rect is a rectangular bounding box. Rect math32.Box2 // Radius is the border radius for rounded rectangles, can be per corner // or one value for all. Radius sides.Floats // Path is the computed clipping path for the Rect and Radius. Path ppath.Path } func NewBounds(x, y, w, h float32, radius sides.Floats) *Bounds { return &Bounds{Rect: math32.B2(x, y, x+w, y+h), Radius: radius} } func NewBoundsRect(rect image.Rectangle, radius sides.Floats) *Bounds { sz := rect.Size() return NewBounds(float32(rect.Min.X), float32(rect.Min.Y), float32(sz.X), float32(sz.Y), radius) } // Context contains all of the rendering constraints / filters / masks // that are applied to elements being rendered. // For SVG compliant rendering, we need a stack of these Context elements // that apply to all elements in the group. // Each level always represents the compounded effects of any parent groups, // with the compounding being performed when a new Context is pushed on the stack. // https://www.w3.org/TR/SVG2/render.html#Grouping type Context struct { // Style has the accumulated style values. // Individual elements inherit from this style. Style styles.Paint // Transform is the accumulated transformation matrix. Transform math32.Matrix2 // Bounds is the rounded rectangle clip boundary. // This is applied to the effective Path prior to adding to Render. Bounds Bounds // ClipPath is the current shape-based clipping path, // in addition to the Bounds, which is applied to the effective Path // prior to adding to Render. ClipPath ppath.Path // Mask is the current masking element, as rendered to a separate image. // This is composited with the rendering output to produce the final result. Mask image.Image // Filter // todo add filtering effects here } // NewContext returns a new Context using given paint style, bounds, and // parent Context. See [Context.Init] for details. func NewContext(sty *styles.Paint, bounds *Bounds, parent *Context) *Context { ctx := &Context{} ctx.Init(sty, bounds, parent) if sty == nil && parent != nil { ctx.Style.UnitContext = parent.Style.UnitContext } return ctx } // Init initializes context based on given style, bounds and parent Context. // If parent is present, then bounds can be nil, in which // case it gets the bounds from the parent. // All the values from the style are used to update the Context, // accumulating anything from the parent. func (ctx *Context) Init(sty *styles.Paint, bounds *Bounds, parent *Context) { if sty != nil { ctx.Style = *sty } else { ctx.Style.Defaults() } if parent == nil { ctx.Transform = sty.Transform ctx.SetBounds(bounds) ctx.ClipPath = sty.ClipPath ctx.Mask = sty.Mask return } ctx.Transform = parent.Transform.Mul(ctx.Style.Transform) ctx.Style.InheritFields(&parent.Style) if bounds == nil { bounds = &parent.Bounds } ctx.SetBounds(bounds) // todo: not clear if following are needed: // ctx.Bounds.Path = ctx.Bounds.Path.And(parent.Bounds.Path) // intersect // ctx.ClipPath = ctx.Style.ClipPath.And(parent.ClipPath) ctx.Mask = parent.Mask // todo: intersect with our own mask } // SetBounds sets the context bounds, and updates the Bounds.Path func (ctx *Context) SetBounds(bounds *Bounds) { ctx.Bounds = *bounds // bsz := bounds.Rect.Size() // ctx.Bounds.Path = *ppath.New().RoundedRectangleSides(bounds.Rect.Min.X, bounds.Rect.Min.Y, bsz.X, bsz.Y, bounds.Radius) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package render // Item is a union interface for render items: // [Path], [Text], [Image], and [ContextPush]. type Item interface { IsRenderItem() } // ContextPush is a [Context] push render item, which can be used by renderers // that track group structure (e.g., SVG). type ContextPush struct { Context Context } // interface assertion. func (p *ContextPush) IsRenderItem() { } // ContextPop is a [Context] pop render item, which can be used by renderers // that track group structure (e.g., SVG). type ContextPop struct { } // interface assertion. func (p *ContextPop) IsRenderItem() { } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package render import ( "cogentcore.org/core/paint/ppath" "cogentcore.org/core/styles" ) // Path is a path drawing render [Item]: responsible for all vector graphics // drawing functionality. type Path struct { // Path specifies the shape(s) to be drawn, using commands: // MoveTo, LineTo, QuadTo, CubeTo, ArcTo, and Close. // Each command has the applicable coordinates appended after it, // like the SVG path element. The coordinates are in the original // units as specified in the Paint drawing commands, without any // transforms applied. See [Path.Transform]. Path ppath.Path // Context has the full accumulated style, transform, etc parameters // for rendering the path, combining the current state context (e.g., // from any higher-level groups) with the current element's style parameters. Context Context } func NewPath(pt ppath.Path, sty *styles.Paint, ctx *Context) *Path { pe := &Path{Path: pt} pe.Context.Init(sty, nil, ctx) return pe } // interface assertion. func (p *Path) IsRenderItem() {} // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package render import ( "reflect" "slices" "cogentcore.org/core/base/reflectx" ) // Render is the sequence of painting [Item]s recorded // from a [paint.Painter] type Render []Item // Clone returns a copy of this Render, // with shallow clones of the Items and Renderers lists. func (pr *Render) Clone() Render { return slices.Clone(*pr) } // Add adds item(s) to render. Filters any nil items. func (pr *Render) Add(item ...Item) *Render { for _, it := range item { if reflectx.IsNil(reflect.ValueOf(it)) { continue } *pr = append(*pr, it) } return pr } // Reset resets back to an empty Render state. // It preserves the existing slice memory for re-use. func (pr *Render) Reset() { *pr = (*pr)[:0] } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package render import ( "cogentcore.org/core/math32" "cogentcore.org/core/styles" "cogentcore.org/core/text/shaped" ) // Text is a text rendering render item. type Text struct { // Text contains shaped Lines of text to be rendered, as produced by a // [shaped.Shaper]. Typically this text is configured so that the // Postion is at the upper left corner of the resulting text rendering. Text *shaped.Lines // Position to render, which typically specifies the upper left corner of // the Text. This is added directly to the offsets and is transformed by the // active transform matrix. See also PositionAbs Position math32.Vector2 // Context has the full accumulated style, transform, etc parameters // for rendering, combining the current state context (e.g., // from any higher-level groups) with the current element's style parameters. Context Context } func NewText(txt *shaped.Lines, sty *styles.Paint, ctx *Context, pos math32.Vector2) *Text { nt := &Text{Text: txt, Position: pos} nt.Context.Init(sty, nil, ctx) return nt } // interface assertion. func (tx *Text) IsRenderItem() {} // 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 rasterx 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 rasterx 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 rasterx 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 rasterx 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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rasterx import ( "image" "image/color" "image/draw" "sync" "cogentcore.org/core/colors" "cogentcore.org/core/math32" "cogentcore.org/core/paint/renderers/rasterx/scan" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/font/opentype" "github.com/go-text/typesetting/shaping" ) var ( // TheGlyphCache is the shared font glyph bitmap render cache. theGlyphCache glyphCache // UseGlyphCache determines if the glyph cache is used. UseGlyphCache = true ) const ( // glyphMaxSize is the max size in either dim for the render mask. glyphMaxSize = 128 // glyphMaskBorder is the extra amount on each side to include around the glyph bounds. glyphMaskBorder = 2 // glyphMaskOffsets is the number of different subpixel offsets to render, in each axis. // The memory usage goes as the square of this number, and 4 produces very good results, // while 2 is acceptable, and is significantly better than 1. 8 is overkill. glyphMaskOffsets = 4 ) func init() { theGlyphCache.init() } // GlyphCache holds cached rendered font glyphs. type glyphCache struct { glyphs map[*font.Face]map[glyphKey]*image.Alpha maxSize image.Point image *image.RGBA scanner *scan.Scanner imgSpanner *scan.ImgSpanner filler *Filler sync.Mutex } // glyphKey is the key for encoding a mask render. type glyphKey struct { gid font.GID // uint32 sx uint8 // size sy uint8 ox uint8 // offset oy uint8 } func (fc *glyphCache) init() { fc.glyphs = make(map[*font.Face]map[glyphKey]*image.Alpha) fc.maxSize = image.Point{glyphMaxSize, glyphMaxSize} sz := fc.maxSize fc.image = image.NewRGBA(image.Rectangle{Max: sz}) fc.imgSpanner = scan.NewImgSpanner(fc.image) fc.scanner = scan.NewScanner(fc.imgSpanner, sz.X, sz.Y) fc.filler = NewFiller(sz.X, sz.Y, fc.scanner) fc.filler.SetWinding(true) fc.filler.SetColor(colors.Uniform(color.Black)) fc.scanner.SetClip(fc.image.Bounds()) } // Glyph returns an existing cached glyph or a newly rendered one, // and the top-left rendering position to use, based on pos arg. // fractional offsets are supported to improve quality. func (gc *glyphCache) Glyph(face *font.Face, g *shaping.Glyph, outline font.GlyphOutline, scale float32, pos math32.Vector2) (*image.Alpha, image.Point) { gc.Lock() defer gc.Unlock() fsize := image.Point{X: int(g.Width.Ceil()), Y: -int(g.Height.Ceil())} size := fsize.Add(image.Point{2 * glyphMaskBorder, 2 * glyphMaskBorder}) if size.X <= 0 || size.X > glyphMaxSize || size.Y <= 0 || size.Y > glyphMaxSize { return nil, image.Point{} } // fmt.Println(face.Describe().Family, g.GlyphID, "wd, ht:", math32.FromFixed(g.Width), -math32.FromFixed(g.Height), "size:", size) // fmt.Printf("g: %#v\n", g) pf := pos.Floor() pi := pf.ToPoint().Sub(image.Point{glyphMaskBorder, glyphMaskBorder}) pi.X += g.XBearing.Round() pi.Y -= g.YBearing.Round() off := pos.Sub(pf) oi := off.MulScalar(glyphMaskOffsets).Floor().ToPoint() // fmt.Println("pos:", pos, "oi:", oi, "pi:", pi) key := glyphKey{gid: g.GlyphID, sx: uint8(fsize.X), sy: uint8(fsize.Y), ox: uint8(oi.X), oy: uint8(oi.Y)} fc, hasfc := gc.glyphs[face] if hasfc { mask := fc[key] if mask != nil { return mask, pi } } else { fc = make(map[glyphKey]*image.Alpha) } mask := gc.renderGlyph(face, g.GlyphID, g, outline, size, scale, oi.X, oi.Y) fc[key] = mask gc.glyphs[face] = fc // fmt.Println(gc.CacheSize()) return mask, pi } // renderGlyph renders the given glyph and caches the result. func (gc *glyphCache) renderGlyph(face *font.Face, gid font.GID, g *shaping.Glyph, outline font.GlyphOutline, size image.Point, scale float32, xo, yo int) *image.Alpha { // clear target: draw.Draw(gc.image, gc.image.Bounds(), colors.Uniform(color.Transparent), image.Point{0, 0}, draw.Src) od := float32(1) / glyphMaskOffsets x := -float32(g.XBearing.Round()) + float32(xo)*od + glyphMaskBorder y := float32(g.YBearing.Round()) + float32(yo)*od + glyphMaskBorder rs := gc.filler rs.Clear() for _, s := range outline.Segments { p0 := math32.Vec2(s.Args[0].X*scale+x, -s.Args[0].Y*scale+y) switch s.Op { case opentype.SegmentOpMoveTo: rs.Start(p0.ToFixed()) case opentype.SegmentOpLineTo: rs.Line(p0.ToFixed()) case opentype.SegmentOpQuadTo: p1 := math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y) rs.QuadBezier(p0.ToFixed(), p1.ToFixed()) case opentype.SegmentOpCubeTo: p1 := math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y) p2 := math32.Vec2(s.Args[2].X*scale+x, -s.Args[2].Y*scale+y) rs.CubeBezier(p0.ToFixed(), p1.ToFixed(), p2.ToFixed()) } } rs.Stop(true) rs.Draw() rs.Clear() bb := image.Rectangle{Max: size} mask := image.NewAlpha(bb) draw.Draw(mask, bb, gc.image, image.Point{}, draw.Src) // fmt.Println("size:", size, *mask) // fmt.Println("render:", gid, size) return mask } // CacheSize reports the total number of bytes used for image masks. // For example, the cogent core docs took about 3.5mb using 4 func (gc *glyphCache) CacheSize() int { gc.Lock() defer gc.Unlock() total := 0 for _, fc := range gc.glyphs { for _, mask := range fc { sz := mask.Bounds().Size() total += sz.X * sz.Y } } return total } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rasterx import ( "image" "slices" "cogentcore.org/core/colors/gradient" "cogentcore.org/core/math32" "cogentcore.org/core/paint/pimage" "cogentcore.org/core/paint/ppath" "cogentcore.org/core/paint/render" "cogentcore.org/core/paint/renderers/rasterx/scan" "cogentcore.org/core/styles/units" ) // Renderer is the rasterx renderer. type Renderer struct { size math32.Vector2 image *image.RGBA // Path is the current path. Path Path // rasterizer -- stroke / fill rendering engine from raster Raster *Dasher // scan scanner Scanner *scan.Scanner // scan spanner ImgSpanner *scan.ImgSpanner } func New(size math32.Vector2) render.Renderer { rs := &Renderer{} rs.SetSize(units.UnitDot, size) return rs } func (rs *Renderer) Image() image.Image { return rs.image } func (rs *Renderer) Source() []byte { return nil } func (rs *Renderer) Size() (units.Units, math32.Vector2) { return units.UnitDot, rs.size } func (rs *Renderer) SetSize(un units.Units, size math32.Vector2) { if rs.size == size { return } rs.size = size psz := size.ToPointCeil() rs.image = image.NewRGBA(image.Rectangle{Max: psz}) rs.ImgSpanner = scan.NewImgSpanner(rs.image) rs.Scanner = scan.NewScanner(rs.ImgSpanner, psz.X, psz.Y) rs.Raster = NewDasher(psz.X, psz.Y, rs.Scanner) } // Render is the main rendering function. func (rs *Renderer) Render(r render.Render) render.Renderer { for _, ri := range r { switch x := ri.(type) { case *render.Path: rs.RenderPath(x) case *pimage.Params: x.Render(rs.image) case *render.Text: rs.RenderText(x) } } return rs } func (rs *Renderer) RenderPath(pt *render.Path) { p := pt.Path if !ppath.ArcToCubeImmediate { p = p.ReplaceArcs() } pc := &pt.Context rs.Scanner.SetClip(pc.Bounds.Rect.ToRect()) PathToRasterx(&rs.Path, p, pt.Context.Transform, math32.Vector2{}) rs.Fill(pt) rs.Stroke(pt) rs.Path.Clear() rs.Raster.Clear() } func PathToRasterx(rs Adder, p ppath.Path, m math32.Matrix2, off math32.Vector2) { for s := p.Scanner(); s.Scan(); { cmd := s.Cmd() end := m.MulVector2AsPoint(s.End()).Add(off) switch cmd { case ppath.MoveTo: rs.Start(end.ToFixed()) case ppath.LineTo: rs.Line(end.ToFixed()) case ppath.QuadTo: cp1 := m.MulVector2AsPoint(s.CP1()).Add(off) rs.QuadBezier(cp1.ToFixed(), end.ToFixed()) case ppath.CubeTo: cp1 := m.MulVector2AsPoint(s.CP1()).Add(off) cp2 := m.MulVector2AsPoint(s.CP2()).Add(off) rs.CubeBezier(cp1.ToFixed(), cp2.ToFixed(), end.ToFixed()) case ppath.Close: rs.Stop(true) } } } func (rs *Renderer) Stroke(pt *render.Path) { pc := &pt.Context sty := &pc.Style if !sty.HasStroke() { return } dash := slices.Clone(sty.Stroke.Dashes) if dash != nil { scx, scy := pc.Transform.ExtractScale() sc := 0.5 * (math32.Abs(scx) + math32.Abs(scy)) for i := range dash { dash[i] *= sc } } sw := rs.StrokeWidth(pt) rs.Raster.SetStroke( math32.ToFixed(sw), math32.ToFixed(sty.Stroke.MiterLimit), capfunc(sty.Stroke.Cap), nil, nil, joinmode(sty.Stroke.Join), dash, 0) rs.Path.AddTo(rs.Raster) rs.SetColor(rs.Raster, pc, sty.Stroke.Color, sty.Stroke.Opacity) rs.Raster.Draw() } func (rs *Renderer) SetColor(sc Scanner, pc *render.Context, clr image.Image, opacity float32) { if g, ok := clr.(gradient.Gradient); ok { fbox := sc.GetPathExtent() lastRenderBBox := image.Rectangle{Min: image.Point{fbox.Min.X.Floor(), fbox.Min.Y.Floor()}, Max: image.Point{fbox.Max.X.Ceil(), fbox.Max.Y.Ceil()}} g.Update(opacity, math32.B2FromRect(lastRenderBBox), pc.Transform) sc.SetColor(clr) } else { if opacity < 1 { sc.SetColor(gradient.ApplyOpacity(clr, opacity)) } else { sc.SetColor(clr) } } } // Fill fills the current path with the current color. Open subpaths // are implicitly closed. The path is preserved after this operation. func (rs *Renderer) Fill(pt *render.Path) { pc := &pt.Context sty := &pc.Style if !sty.HasFill() { return } rf := &rs.Raster.Filler rf.SetWinding(sty.Fill.Rule == ppath.NonZero) rs.Path.AddTo(rf) rs.SetColor(rf, pc, sty.Fill.Color, sty.Fill.Opacity) rf.Draw() rf.Clear() } func MeanScale(m math32.Matrix2) float32 { scx, scy := m.ExtractScale() return 0.5 * (math32.Abs(scx) + math32.Abs(scy)) } // StrokeWidth obtains the current stoke width subject to transform (or not // depending on VecEffNonScalingStroke) func (rs *Renderer) StrokeWidth(pt *render.Path) float32 { pc := &pt.Context sty := &pc.Style dw := sty.Stroke.Width.Dots if dw == 0 { return dw } if sty.VectorEffect == ppath.VectorEffectNonScalingStroke { return dw } sc := MeanScale(pt.Context.Transform) lw := math32.Max(sc*dw, sty.Stroke.MinWidth.Dots) return lw } func capfunc(st ppath.Caps) CapFunc { switch st { case ppath.CapButt: return ButtCap case ppath.CapRound: return RoundCap case ppath.CapSquare: return SquareCap } return nil } func joinmode(st ppath.Joins) JoinMode { switch st { case ppath.JoinMiter: return Miter case ppath.JoinMiterClip: return MiterClip case ppath.JoinRound: return Round case ppath.JoinBevel: return Bevel case ppath.JoinArcs: return Arc case ppath.JoinArcsClip: return ArcClip } return Arc } // 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) 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 rasterx 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 rasterx 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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rasterx import ( "image" "image/color" "image/draw" _ "image/jpeg" // load image formats for users of the API _ "image/png" "cogentcore.org/core/colors" "cogentcore.org/core/math32" "cogentcore.org/core/paint/render" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/shaped/shapers/shapedgt" "cogentcore.org/core/text/textpos" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/font/opentype" "github.com/go-text/typesetting/shaping" _ "golang.org/x/image/tiff" // load image formats for users of the API ) // RenderText rasterizes the given Text func (rs *Renderer) RenderText(txt *render.Text) { // pr := profile.Start("RenderText") rs.TextLines(&txt.Context, txt.Text, txt.Position) // pr.End() } // TextLines rasterizes the given shaped.Lines. // The text will be drawn starting at the start pixel position, which specifies the // left baseline location of the first text item.. func (rs *Renderer) TextLines(ctx *render.Context, lns *shaped.Lines, pos math32.Vector2) { m := ctx.Transform identity := m == math32.Identity2() off := pos.Add(lns.Offset) rs.Scanner.SetClip(ctx.Bounds.Rect.ToRect()) // tbb := lns.Bounds.Translate(off) // rs.StrokeBounds(ctx, tbb, colors.Red) clr := colors.Uniform(lns.Color) for li := range lns.Lines { ln := &lns.Lines[li] rs.TextLine(ctx, ln, lns, clr, off, identity) } } // TextLine rasterizes the given shaped.Line. func (rs *Renderer) TextLine(ctx *render.Context, ln *shaped.Line, lns *shaped.Lines, clr image.Image, off math32.Vector2, identity bool) { start := off.Add(ln.Offset) off = start // tbb := ln.Bounds.Translate(off) // rs.StrokeBounds(ctx, tbb, colors.Blue) for ri := range ln.Runs { run := ln.Runs[ri].(*shapedgt.Run) rs.TextRunRegions(ctx, run, ln, lns, off) if run.Direction.IsVertical() { off.Y += run.Advance() } else { off.X += run.Advance() } } off = start for ri := range ln.Runs { run := ln.Runs[ri].(*shapedgt.Run) rs.TextRun(ctx, run, ln, lns, clr, off, identity) if run.Direction.IsVertical() { off.Y += run.Advance() } else { off.X += run.Advance() } } } // TextRegionFill fills given regions within run with given fill color. func (rs *Renderer) TextRegionFill(ctx *render.Context, run *shapedgt.Run, off math32.Vector2, fill image.Image, ranges []textpos.Range) { if fill == nil { return } for _, sel := range ranges { rsel := sel.Intersect(run.Runes()) if rsel.Len() == 0 { continue } fi := run.FirstGlyphAt(rsel.Start) li := run.LastGlyphAt(rsel.End - 1) if fi >= 0 && li >= fi { sbb := run.GlyphRegionBounds(fi, li).Canon() rs.FillBounds(ctx, sbb.Translate(off), fill) } } } // TextRunRegions draws region fills for given run. func (rs *Renderer) TextRunRegions(ctx *render.Context, run *shapedgt.Run, ln *shaped.Line, lns *shaped.Lines, off math32.Vector2) { // dir := run.Direction rbb := run.MaxBounds.Translate(off) if run.Background != nil { rs.FillBounds(ctx, rbb, run.Background) } rs.TextRegionFill(ctx, run, off, lns.SelectionColor, ln.Selections) rs.TextRegionFill(ctx, run, off, lns.HighlightColor, ln.Highlights) } // TextRun rasterizes the given text run into the output image using the // font face set in the shaping. // The text will be drawn starting at the start pixel position. func (rs *Renderer) TextRun(ctx *render.Context, run *shapedgt.Run, ln *shaped.Line, lns *shaped.Lines, clr image.Image, off math32.Vector2, identity bool) { // dir := run.Direction rbb := run.MaxBounds.Translate(off) fill := clr if run.FillColor != nil { fill = run.FillColor } stroke := run.StrokeColor fsz := math32.FromFixed(run.Size) lineW := max(fsz/16, 1) // 1 at 16, bigger if biggerr if run.Math.Path != nil { rs.Path.Clear() PathToRasterx(&rs.Path, *run.Math.Path, ctx.Transform, off) rf := &rs.Raster.Filler rf.SetWinding(true) rf.SetColor(fill) rs.Path.AddTo(rf) rf.Draw() rf.Clear() return } if run.Decoration.HasFlag(rich.Underline) || run.Decoration.HasFlag(rich.DottedUnderline) { dash := []float32{2, 2} if run.Decoration.HasFlag(rich.Underline) { dash = nil } if run.Direction.IsVertical() { } else { dec := off.Y + 3 rs.StrokeTextLine(ctx, math32.Vec2(rbb.Min.X, dec), math32.Vec2(rbb.Max.X, dec), lineW, fill, dash) } } if run.Decoration.HasFlag(rich.Overline) { if run.Direction.IsVertical() { } else { dec := off.Y - 0.7*rbb.Size().Y rs.StrokeTextLine(ctx, math32.Vec2(rbb.Min.X, dec), math32.Vec2(rbb.Max.X, dec), lineW, fill, nil) } } for gi := range run.Glyphs { g := &run.Glyphs[gi] pos := off.Add(math32.Vec2(math32.FromFixed(g.XOffset), -math32.FromFixed(g.YOffset))) bb := run.GlyphBoundsBox(g).Translate(off) // rs.StrokeBounds(ctx, bb, colors.Yellow) data := run.Face.GlyphData(g.GlyphID) switch format := data.(type) { case font.GlyphOutline: rs.GlyphOutline(ctx, run, g, format, fill, stroke, bb, pos, identity) case font.GlyphBitmap: rs.GlyphBitmap(ctx, run, g, format, fill, stroke, bb, pos, identity) case font.GlyphSVG: rs.GlyphSVG(ctx, run, g, format.Source, bb, pos, identity) } off.X += math32.FromFixed(g.XAdvance) off.Y -= math32.FromFixed(g.YAdvance) } if run.Decoration.HasFlag(rich.LineThrough) { if run.Direction.IsVertical() { } else { dec := off.Y - 0.2*rbb.Size().Y rs.StrokeTextLine(ctx, math32.Vec2(rbb.Min.X, dec), math32.Vec2(rbb.Max.X, dec), lineW, fill, nil) } } } func (rs *Renderer) GlyphOutline(ctx *render.Context, run *shapedgt.Run, g *shaping.Glyph, outline font.GlyphOutline, fill, stroke image.Image, bb math32.Box2, pos math32.Vector2, identity bool) { scale := math32.FromFixed(run.Size) / float32(run.Face.Upem()) x := pos.X // note: has offsets already added y := pos.Y if len(outline.Segments) == 0 { // fmt.Println("nil path:", g.GlyphID) return } wd := math32.FromFixed(g.Width) xadv := math32.Abs(math32.FromFixed(g.XAdvance)) if wd > xadv { if run.Font.Style(&ctx.Style.Text).Family == rich.Monospace { scale *= 0.95 * xadv / wd } } if UseGlyphCache && identity && stroke == nil { mask, pi := theGlyphCache.Glyph(run.Face, g, outline, scale, pos) if mask != nil { rs.GlyphMask(ctx, run, g, fill, stroke, bb, pi, mask) return } } rs.Path.Clear() m := ctx.Transform for _, s := range outline.Segments { p0 := m.MulVector2AsPoint(math32.Vec2(s.Args[0].X*scale+x, -s.Args[0].Y*scale+y)) switch s.Op { case opentype.SegmentOpMoveTo: rs.Path.Start(p0.ToFixed()) case opentype.SegmentOpLineTo: rs.Path.Line(p0.ToFixed()) case opentype.SegmentOpQuadTo: p1 := m.MulVector2AsPoint(math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y)) rs.Path.QuadBezier(p0.ToFixed(), p1.ToFixed()) case opentype.SegmentOpCubeTo: p1 := m.MulVector2AsPoint(math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y)) p2 := m.MulVector2AsPoint(math32.Vec2(s.Args[2].X*scale+x, -s.Args[2].Y*scale+y)) rs.Path.CubeBezier(p0.ToFixed(), p1.ToFixed(), p2.ToFixed()) } } rs.Path.Stop(true) if fill != nil { rf := &rs.Raster.Filler rf.SetWinding(true) rf.SetColor(fill) rs.Path.AddTo(rf) rf.Draw() rf.Clear() } if stroke != nil { sw := math32.FromFixed(run.Size) / 32.0 // scale with font size rs.Raster.SetStroke( math32.ToFixed(sw), math32.ToFixed(10), ButtCap, nil, nil, Miter, nil, 0) rs.Path.AddTo(rs.Raster) rs.Raster.SetColor(stroke) rs.Raster.Draw() rs.Raster.Clear() } rs.Path.Clear() } func (rs *Renderer) GlyphMask(ctx *render.Context, run *shapedgt.Run, g *shaping.Glyph, fill, stroke image.Image, bb math32.Box2, pos image.Point, mask *image.Alpha) error { mbb := mask.Bounds() dbb := mbb.Add(pos) ibb := dbb.Intersect(ctx.Bounds.Rect.ToRect()) if ibb == (image.Rectangle{}) { return nil } mp := ibb.Min.Sub(dbb.Min) draw.DrawMask(rs.image, ibb, fill, image.Point{}, mask, mp, draw.Over) return nil } // StrokeBounds strokes a bounding box in the given color. Useful for debugging. func (rs *Renderer) StrokeBounds(ctx *render.Context, bb math32.Box2, clr color.Color) { rs.Raster.SetStroke( math32.ToFixed(1), math32.ToFixed(10), ButtCap, nil, nil, Miter, nil, 0) rs.Raster.SetColor(colors.Uniform(clr)) m := ctx.Transform rs.Raster.Start(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Min.Y)).ToFixed()) rs.Raster.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Min.Y)).ToFixed()) rs.Raster.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Max.Y)).ToFixed()) rs.Raster.Line(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Max.Y)).ToFixed()) rs.Raster.Stop(true) rs.Raster.Draw() rs.Raster.Clear() } // StrokeTextLine strokes a line for text decoration. func (rs *Renderer) StrokeTextLine(ctx *render.Context, sp, ep math32.Vector2, width float32, clr image.Image, dash []float32) { m := ctx.Transform sp = m.MulVector2AsPoint(sp) ep = m.MulVector2AsPoint(ep) width *= MeanScale(m) rs.Raster.SetStroke( math32.ToFixed(width), math32.ToFixed(10), ButtCap, nil, nil, Miter, dash, 0) rs.Raster.SetColor(clr) rs.Raster.Start(sp.ToFixed()) rs.Raster.Line(ep.ToFixed()) rs.Raster.Stop(false) rs.Raster.Draw() rs.Raster.Clear() } // FillBounds fills a bounding box in the given color. func (rs *Renderer) FillBounds(ctx *render.Context, bb math32.Box2, clr image.Image) { rf := &rs.Raster.Filler rf.SetColor(clr) m := ctx.Transform rf.Start(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Min.Y)).ToFixed()) rf.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Min.Y)).ToFixed()) rf.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Max.Y)).ToFixed()) rf.Line(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Max.Y)).ToFixed()) rf.Stop(true) rf.Draw() rf.Clear() } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rasterx import ( "bytes" "image" "image/color" "image/draw" "cogentcore.org/core/colors" "cogentcore.org/core/math32" "cogentcore.org/core/paint/render" "cogentcore.org/core/text/shaped/shapers/shapedgt" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/shaping" scale "golang.org/x/image/draw" ) var bitmapGlyphCache map[glyphKey]*image.RGBA func (rs *Renderer) GlyphBitmap(ctx *render.Context, run *shapedgt.Run, g *shaping.Glyph, bitmap font.GlyphBitmap, fill, stroke image.Image, bb math32.Box2, pos math32.Vector2, identity bool) error { if bitmapGlyphCache == nil { bitmapGlyphCache = make(map[glyphKey]*image.RGBA) } // todo: this needs serious work to function with transforms x := pos.X y := pos.Y top := y - math32.FromFixed(g.YBearing) bottom := top - math32.FromFixed(g.Height) right := x + math32.FromFixed(g.Width) dbb := image.Rect(int(x), int(top), int(right), int(bottom)) ibb := dbb.Intersect(ctx.Bounds.Rect.ToRect()) if ibb == (image.Rectangle{}) { return nil } fam := run.Font.Style(&ctx.Style.Text).Family size := dbb.Size() gk := glyphKey{gid: g.GlyphID, sx: uint8(size.Y / 256), sy: uint8(size.Y % 256), ox: uint8(fam)} img, ok := bitmapGlyphCache[gk] if !ok { img = image.NewRGBA(image.Rectangle{Max: size}) switch bitmap.Format { case font.BlackAndWhite: rec := image.Rect(0, 0, bitmap.Width, bitmap.Height) sub := image.NewPaletted(rec, color.Palette{color.Transparent, colors.ToUniform(fill)}) for i := range sub.Pix { sub.Pix[i] = bitAt(bitmap.Data, i) } // note: NearestNeighbor is better than bilinear scale.NearestNeighbor.Scale(img, img.Bounds(), sub, sub.Bounds(), draw.Src, nil) case font.JPG, font.PNG, font.TIFF: pix, _, err := image.Decode(bytes.NewReader(bitmap.Data)) if err != nil { return err } scale.NearestNeighbor.Scale(img, img.Bounds(), pix, pix.Bounds(), draw.Src, nil) } bitmapGlyphCache[gk] = img } sp := ibb.Min.Sub(dbb.Min) draw.Draw(rs.image, ibb, img, sp, draw.Over) if bitmap.Outline != nil { rs.GlyphOutline(ctx, run, g, *bitmap.Outline, fill, stroke, bb, pos, identity) } return nil } // bitAt returns the bit at the given index in the byte slice. func bitAt(b []byte, i int) byte { return (b[i/8] >> (7 - i%8)) & 1 } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rasterx import ( "bytes" "fmt" "image" "image/draw" "cogentcore.org/core/base/errors" "cogentcore.org/core/math32" "cogentcore.org/core/paint/render" "cogentcore.org/core/svg" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped/shapers/shapedgt" "github.com/go-text/typesetting/shaping" ) var svgGlyphCache map[glyphKey]image.Image func (rs *Renderer) GlyphSVG(ctx *render.Context, run *shapedgt.Run, g *shaping.Glyph, svgCmds []byte, bb math32.Box2, pos math32.Vector2, identity bool) { if svgGlyphCache == nil { svgGlyphCache = make(map[glyphKey]image.Image) } size := run.Size.Floor() fsize := image.Point{X: size, Y: size} scale := 82.0 / float32(run.Face.Upem()) fam := run.Font.Style(&ctx.Style.Text).Family if fam == rich.Monospace { scale *= 0.8 } gk := glyphKey{gid: g.GlyphID, sx: uint8(size / 256), sy: uint8(size % 256), ox: uint8(fam)} img, ok := svgGlyphCache[gk] if !ok { sv := svg.NewSVG(math32.FromPoint(fsize)) sv.GroupFilter = fmt.Sprintf("glyph%d", g.GlyphID) // critical: for filtering items with many glyphs b := bytes.NewBuffer(svgCmds) err := sv.ReadXML(b) errors.Log(err) sv.Translate.Y = float32(run.Face.Upem()) sv.Scale = scale img = sv.RenderImage() svgGlyphCache[gk] = img } left := int(math32.Round(pos.X + math32.FromFixed(g.XBearing))) desc := run.Output.LineBounds.Descent top := int(math32.Round(pos.Y - math32.FromFixed(g.YBearing+desc) - float32(fsize.Y))) dbb := img.Bounds().Add(image.Point{left, top}) ibb := dbb.Intersect(ctx.Bounds.Rect.ToRect()) if ibb == (image.Rectangle{}) { return } sp := ibb.Min.Sub(dbb.Min) draw.Draw(rs.image, ibb, img, sp, draw.Over) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package renderers import ( "cogentcore.org/core/paint" "cogentcore.org/core/paint/renderers/svgrender" _ "cogentcore.org/core/text/shaped/shapers" ) func init() { paint.NewSVGRenderer = svgrender.New } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build !js package renderers import ( "cogentcore.org/core/paint" "cogentcore.org/core/paint/renderers/rasterx" _ "cogentcore.org/core/text/shaped/shapers" ) func init() { paint.NewSourceRenderer = rasterx.New paint.NewImageRenderer = rasterx.New } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package svgrender import ( "bytes" "image" "maps" "cogentcore.org/core/base/iox/imagex" "cogentcore.org/core/base/reflectx" "cogentcore.org/core/base/stack" "cogentcore.org/core/colors/gradient" "cogentcore.org/core/math32" "cogentcore.org/core/paint" "cogentcore.org/core/paint/pimage" "cogentcore.org/core/paint/render" "cogentcore.org/core/styles/units" "cogentcore.org/core/svg" "cogentcore.org/core/text/shaped/shapers/shapedgt" ) // Renderer is the SVG renderer. type Renderer struct { size math32.Vector2 SVG *svg.SVG // gpStack is a stack of groups used while building the svg gpStack stack.Stack[*svg.Group] } func New(size math32.Vector2) render.Renderer { rs := &Renderer{} rs.SetSize(units.UnitDot, size) return rs } func (rs *Renderer) Image() image.Image { if rs.SVG == nil { return nil } pc := rs.SVG.Render(nil) ir := paint.NewImageRenderer(rs.size) ir.Render(pc.RenderDone()) return ir.Image() } func (rs *Renderer) Source() []byte { if rs.SVG == nil { return nil } var b bytes.Buffer rs.SVG.WriteXML(&b, true) return b.Bytes() } func (rs *Renderer) Size() (units.Units, math32.Vector2) { return units.UnitDot, rs.size } func (rs *Renderer) SetSize(un units.Units, size math32.Vector2) { if rs.size == size { return } rs.size = size } // Render is the main rendering function. func (rs *Renderer) Render(r render.Render) render.Renderer { rs.SVG = svg.NewSVG(rs.size) rs.gpStack = nil bg := svg.NewGroup(rs.SVG.Root) rs.gpStack.Push(bg) for _, ri := range r { switch x := ri.(type) { case *render.Path: rs.RenderPath(x) case *pimage.Params: rs.RenderImage(x) case *render.Text: rs.RenderText(x) case *render.ContextPush: rs.PushContext(x) case *render.ContextPop: rs.PopContext(x) } } // pc := paint.NewPainter(rs.size) // rs.SVG.Render(pc) // rs.rend = pc.RenderDone() return rs } func (rs *Renderer) PushGroup() *svg.Group { cg := rs.gpStack.Peek() g := svg.NewGroup(cg) rs.gpStack.Push(g) return g } func (rs *Renderer) RenderPath(pt *render.Path) { p := pt.Path pc := &pt.Context cg := rs.gpStack.Peek() sp := svg.NewPath(cg) sp.Data = p.Clone() props := map[string]any{} pt.Context.Style.GetProperties(props) if !pc.Transform.IsIdentity() { props["transform"] = pc.Transform.String() } sp.Properties = props // rs.Scanner.SetClip(pc.Bounds.Rect.ToRect()) } func (rs *Renderer) PushContext(pt *render.ContextPush) { pc := &pt.Context g := rs.PushGroup() g.Paint.Transform = pc.Transform } func (rs *Renderer) PopContext(pt *render.ContextPop) { rs.gpStack.Pop() } func (rs *Renderer) RenderText(pt *render.Text) { pc := &pt.Context cg := rs.gpStack.Peek() tg := svg.NewGroup(cg) props := map[string]any{} pt.Context.Style.GetProperties(props) if !pc.Transform.IsIdentity() { props["transform"] = pc.Transform.String() } pos := pt.Position tx := pt.Text.Source txt := tx.Join() for li := range pt.Text.Lines { ln := &pt.Text.Lines[li] lpos := pos.Add(ln.Offset) rpos := lpos for ri := range ln.Runs { run := ln.Runs[ri].(*shapedgt.Run) rs := run.Runes().Start re := run.Runes().End si, _, _ := tx.Index(rs) sty, _ := tx.Span(si) rtxt := txt[rs:re] st := svg.NewText(tg) st.Text = string(rtxt) rprops := maps.Clone(props) if pc.Style.UnitContext.DPI != 160 { sty.Size *= pc.Style.UnitContext.DPI / 160 } pt.Context.Style.Text.ToProperties(sty, rprops) rprops["x"] = reflectx.ToString(rpos.X) rprops["y"] = reflectx.ToString(rpos.Y) st.Pos = rpos st.Properties = rprops rpos.X += run.Advance() } } } func (rs *Renderer) RenderImage(pr *pimage.Params) { usrc := imagex.Unwrap(pr.Source) umask := imagex.Unwrap(pr.Mask) cg := rs.gpStack.Peek() nilSrc := usrc == nil if r, ok := usrc.(*image.RGBA); ok && r == nil { nilSrc = true } if pr.Rect == (image.Rectangle{}) { pr.Rect = image.Rectangle{Max: rs.size.ToPoint()} } // todo: handle masks! // Fast path for [image.Uniform] if u, ok := usrc.(*image.Uniform); nilSrc || ok && umask == nil { _ = u return } if gr, ok := usrc.(gradient.Gradient); ok { _ = gr // todo: handle: return } sz := pr.Rect.Size() simg := svg.NewImage(cg) simg.SetImage(usrc, float32(sz.X), float32(sz.Y)) simg.Pos = math32.FromPoint(pr.Rect.Min) // todo: ViewBox? } // 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" "log/slog" "cogentcore.org/core/base/iox/imagex" "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" "cogentcore.org/core/paint/render" "cogentcore.org/core/styles" "cogentcore.org/core/styles/sides" ) var ( // NewSourceRenderer returns the [composer.Source] renderer // for [Painter] rendering, for the current platform. NewSourceRenderer func(size math32.Vector2) render.Renderer // NewImageRenderer returns a painter renderer for generating // images locally in Go regardless of platform. NewImageRenderer func(size math32.Vector2) render.Renderer // NewSVGRenderer returns a structured SVG renderer that can // generate an SVG vector graphics document from painter content. NewSVGRenderer func(size math32.Vector2) render.Renderer ) // RenderToImage is a convenience function that renders the current // accumulated painter actions to an image using a [NewImageRenderer], // and returns the Image() call from that renderer. // The image is wrapped by [imagex.WrapJS] so that it is ready to be // used efficiently for subsequent rendering actions on the JS (web) platform. func RenderToImage(pc *Painter) image.Image { rd := NewImageRenderer(pc.Size) return imagex.WrapJS(rd.Render(pc.RenderDone()).Image()) } // RenderToSVG is a convenience function that renders the current // accumulated painter actions to an SVG document using a // [NewSVGRenderer].n func RenderToSVG(pc *Painter) []byte { rd := NewSVGRenderer(pc.Size) return rd.Render(pc.RenderDone()).Source() } // The State holds all the current rendering state information used // while painting. The [Paint] embeds a pointer to this. type State struct { // Size in dots (true pixels) as specified during Init. Size math32.Vector2 // Stack provides the SVG "stacking context" as a stack of [Context]s. // There is always an initial base-level Context element for the overall // rendering context. Stack []*render.Context // Render holds the current [render.PaintRender] state that we are building. // and has the list of [render.Renderer]s that we render to. Render render.Render // Path is the current path state we are adding to. Path ppath.Path } // Init initializes the rendering state, creating a new Stack // with an initial baseline context using given size and styles. // Size is used to set the bounds for clipping rendering, assuming // units are image dots (true pixels), which is typical. // This should be called whenever the size changes. func (rs *State) Init(sty *styles.Paint, size math32.Vector2) { rs.Size = size bounds := render.NewBounds(0, 0, size.X, size.Y, sides.Floats{}) rs.Stack = []*render.Context{render.NewContext(sty, bounds, nil)} rs.Render = nil rs.Path = nil } // RenderDone should be called when the full set of rendering // for this painter is done. It returns a self-contained // [render.Render] representing the entire rendering state, // suitable for rendering by passing to a [render.Renderer]. // It resets the current painter state so that it is ready for // new rendering. func (rs *State) RenderDone() render.Render { npr := rs.Render.Clone() rs.Render.Reset() rs.Path.Reset() if len(rs.Stack) > 1 { // ensure back to baseline stack rs.Stack = rs.Stack[:1] } return npr } // Context() returns the currently active [render.Context] state (top of Stack). func (rs *State) Context() *render.Context { return rs.Stack[len(rs.Stack)-1] } // PushContext pushes a new [render.Context] onto the stack using given styles and bounds. // The transform from the style will be applied to all elements rendered // within this group, along with the other group properties. // This adds the Context to the current Render state as well, so renderers // that track grouping will track this. // Must protect within render mutex lock (see Lock version). func (rs *State) PushContext(sty *styles.Paint, bounds *render.Bounds) *render.Context { parent := rs.Context() g := render.NewContext(sty, bounds, parent) rs.Stack = append(rs.Stack, g) rs.Render.Add(&render.ContextPush{Context: *g}) return g } // PopContext pops the current Context off of the Stack. func (rs *State) PopContext() { n := len(rs.Stack) if n == 1 { slog.Error("programmer error: paint.State.PopContext: stack is at base starting point") return } rs.Stack = rs.Stack[:n-1] rs.Render.Add(&render.ContextPop{}) } // 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 // ScrollableUnattended means it can be Scrolled and Slided without // Focused or Attended state. This is true by default only for Frames. ScrollableUnattended ) var ( // Pressable is the list of abilities that makes something Pressable Pressable = []Abilities{Selectable, Activatable, DoubleClickable, TripleClickable, Draggable, Slideable, Checkable, Clickable} pressableBits = []enums.BitFlag{Selectable, Activatable, DoubleClickable, TripleClickable, Draggable, Slideable, Checkable, Clickable} ) // 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 enums.HasAnyFlags((*int64)(ab), pressableBits...) } // 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, 15} // AbilitiesN is the highest valid value for type Abilities, plus one. const AbilitiesN Abilities = 16 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, `ScrollableUnattended`: 15} 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.`, 15: `ScrollableUnattended means it can be Scrolled and Slided without Focused or Attended state. This is true by default only for Frames.`} 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`, 15: `ScrollableUnattended`} // 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/sides" "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.Sides[BorderStyles] // Width specifies the width of the border Width sides.Values `display:"inline"` // Radius specifies the radius (rounding) of the corners Radius sides.Values `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.Painter.DrawStdBox] and // all standard GUI elements. Offset sides.Values `display:"inline"` // Color specifies the color of the border Color sides.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 = sides.NewValues(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 = sides.NewValues(units.Dp(4), units.Dp(4), units.Zero(), units.Zero()) // BorderRadiusSmall indicates to use small // 8dp rounded corners BorderRadiusSmall = sides.NewValues(units.Dp(8)) // BorderRadiusMedium indicates to use medium // 12dp rounded corners BorderRadiusMedium = sides.NewValues(units.Dp(12)) // BorderRadiusLarge indicates to use large // 16dp rounded corners BorderRadiusLarge = sides.NewValues(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 = sides.NewValues(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 = sides.NewValues(units.Dp(16), units.Dp(16), units.Zero(), units.Zero()) // BorderRadiusExtraLarge indicates to use extra large // 28dp rounded corners BorderRadiusExtraLarge = sides.NewValues(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 = sides.NewValues(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 = sides.NewValues(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() sides.Floats { // 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 sides.NewFloats( 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() sides.Floats { return BoxShadowMargin(s.BoxShadow) } // MaxBoxShadowMargin returns the maximum effective box // shadow margin of the style, calculated through [Shadow.Margin] func (s *Style) MaxBoxShadowMargin() sides.Floats { 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) sides.Floats { max := sides.Floats{} 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" "cogentcore.org/core/text/rich" ) // 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()) } // todo: // 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 == rich.Medium { add("font-weight", "500") } else { add("font-weight", s.Font.Weight.String()) } add("line-height", fmt.Sprintf("%g", s.Text.LineHeight)) 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 _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 _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") } // 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/slog" "cogentcore.org/core/colors" "cogentcore.org/core/math32" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" ) // IMPORTANT: any changes here must be updated in style_properties.go StyleFontFuncs // Font contains all font styling information. // Most of font information is inherited. type Font struct { //types:add // Size of font to render (inherited). // Converted to points when getting font to use. Size units.Value // Family indicates the generic family of typeface to use, where the // specific named values to use for each are provided in the [Settings], // or [CustomFont] for [Custom]. Family rich.Family // CustomFont specifies the Custom font name for Family = Custom. CustomFont rich.FontName // Slant allows italic or oblique faces to be selected. Slant rich.Slants // Weights are the degree of blackness or stroke thickness of a font. // This value ranges from 100.0 to 900.0, with 400.0 as normal. Weight rich.Weights // Stretch is the width of a font as an approximate fraction of the normal width. // Widths range from 0.5 to 2.0 inclusive, with 1.0 as the normal width. Stretch rich.Stretch // Decorations are underline, line-through, etc, as bit flags // that must be set using [Decorations.SetFlag]. Decoration rich.Decorations } func (fs *Font) Defaults() { fs.Size.Dp(16) fs.Weight = rich.Normal fs.Stretch = rich.StretchNormal } // InheritFields from parent func (fs *Font) InheritFields(parent *Font) { if parent.Size.Value != 0 { fs.Size = parent.Size } fs.Family = parent.Family fs.CustomFont = parent.CustomFont fs.Slant = parent.Slant fs.Weight = parent.Weight fs.Stretch = parent.Stretch fs.Decoration = parent.Decoration } // 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) } // SetUnitContext sets the font-specific information in the given // units.Context, based on the given styles. Just uses standardized // fractions of the font size for the other less common units such as ex, ch. func (fs *Font) SetUnitContext(uc *units.Context) { fsz := math32.Round(fs.Size.Dots) if fsz == 0 { fsz = 16 } uc.SetFont(fsz) } // SetDecoration sets text decoration (underline, etc), // which uses bitflags to allow multiple combinations. func (fs *Font) SetDecoration(deco ...rich.Decorations) *Font { for _, d := range deco { fs.Decoration.SetFlag(true, d) } return fs } // FontHeight returns the font height in dots (actual pixels). // Only valid after ToDots has been called, as final step of styling. func (fs *Font) FontHeight() float32 { return math32.Round(fs.Size.Dots) } // SetRich sets the rich.Style from font style. func (fs *Font) SetRich(sty *rich.Style) { sty.Family = fs.Family sty.Slant = fs.Slant sty.Weight = fs.Weight sty.Stretch = fs.Stretch sty.Decoration = fs.Decoration } // SetRichText sets the rich.Style and text.Style properties from the style props. func (s *Style) SetRichText(sty *rich.Style, tsty *text.Style) { s.Font.SetRich(sty) s.Text.SetText(tsty) tsty.FontSize = s.Font.Size tsty.CustomFont = s.Font.CustomFont if s.Color != nil { clr := colors.ApplyOpacity(colors.ToUniform(s.Color), s.Opacity) tsty.Color = clr } // note: no default background color here } // NewRichText sets the rich.Style and text.Style properties from the style props. func (s *Style) NewRichText() (sty *rich.Style, tsty *text.Style) { sty = rich.NewStyle() tsty = text.NewStyle() s.SetRichText(sty, tsty) 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 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.Linear) 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" "cogentcore.org/core/colors" "cogentcore.org/core/paint/ppath" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" ) // Paint provides the styling parameters for SVG-style rendering, // including the Path stroke and fill properties, and font and text // properties. type Paint struct { //types:add Path // Font selects font properties. Font rich.Style // Text has the text styling settings. Text text.Style // ClipPath is a clipping path for this item. ClipPath ppath.Path // Mask is a rendered image of the mask for this item. Mask image.Image } func NewPaint() *Paint { pc := &Paint{} pc.Defaults() return pc } // NewPaintWithContext returns a new Paint style with [units.Context] // initialized from given. Pass the Styles context for example. func NewPaintWithContext(uc *units.Context) *Paint { pc := NewPaint() pc.UnitContext = *uc return pc } func (pc *Paint) Defaults() { pc.Path.Defaults() pc.Font.Defaults() pc.Text.Defaults() } // CopyStyleFrom copies styles from another paint func (pc *Paint) CopyStyleFrom(cp *Paint) { pc.Path.CopyStyleFrom(&cp.Path) pc.Font = cp.Font pc.Text = cp.Text } // InheritFields from parent func (pc *Paint) InheritFields(parent *Paint) { pc.Font.InheritFields(&parent.Font) pc.Text.InheritFields(&parent.Text) } // SetProperties 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) SetProperties(parent *Paint, properties map[string]any, ctxt colors.Context) { if !pc.StyleSet && parent != nil { // first time pc.InheritFields(parent) } pc.fromProperties(parent, properties, ctxt) pc.PropertiesNil = (len(properties) == 0) pc.StyleSet = true } // GetProperties gets properties values from current style settings, // for any non-default settings, setting name-value pairs in given map, // which must be non-nil. func (pc *Paint) GetProperties(properties map[string]any) { pc.toProperties(properties) } func (pc *Paint) FromStyle(st *Style) { pc.UnitContext = st.UnitContext st.SetRichText(&pc.Font, &pc.Text) } // ToDotsImpl runs ToDots on unit values, to compile down to raw pixels func (pc *Paint) ToDotsImpl(uc *units.Context) { pc.Path.ToDotsImpl(uc) // pc.Font.ToDots(uc) pc.Text.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)) // todo: need a shaper here to get SetUnitContext call // pc.Font.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 } } // 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/paint/ppath" "cogentcore.org/core/styles/styleprops" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" ) /////// see style_props.go for master version // fromProperties sets style field values based on map[string]any properties func (pc *Path) fromProperties(parent *Path, 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 := styleprops.InhInit(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.Stroke, key, val, &parent.Stroke, cc) } else { sfunc(&pc.Stroke, key, val, nil, cc) } continue } if sfunc, ok := styleFillFuncs[key]; ok { if parent != nil { sfunc(&pc.Fill, key, val, &parent.Fill, cc) } else { sfunc(&pc.Fill, key, val, nil, cc) } continue } if sfunc, ok := stylePathFuncs[key]; ok { sfunc(pc, key, val, parent, cc) continue } } } // fromProperties sets style field values based on map[string]any properties func (pc *Paint) fromProperties(parent *Paint, properties map[string]any, cc colors.Context) { var ppath *Path var pfont *rich.Style var ptext *text.Style if parent != nil { ppath = &parent.Path pfont = &parent.Font ptext = &parent.Text } pc.Path.fromProperties(ppath, properties, cc) pc.Font.FromProperties(pfont, properties, cc) pc.Text.FromProperties(ptext, properties, cc) for key, val := range properties { _ = val if len(key) == 0 { continue } if key[0] == '#' || key[0] == '.' || key[0] == ':' || key[0] == '_' { continue } // todo: add others here } } //////// Stroke // styleStrokeFuncs are functions for styling the Stroke object var styleStrokeFuncs = map[string]styleprops.Func{ "stroke": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Stroke) if inh, init := styleprops.InhInit(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": styleprops.Float(float32(1), func(obj *Stroke) *float32 { return &(obj.Opacity) }), "stroke-width": styleprops.Units(units.Dp(1), func(obj *Stroke) *units.Value { return &(obj.Width) }), "stroke-min-width": styleprops.Units(units.Dp(1), func(obj *Stroke) *units.Value { return &(obj.MinWidth) }), "stroke-dasharray": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Stroke) if inh, init := styleprops.InhInit(val, parent); inh || init { 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": styleprops.Enum(ppath.CapButt, func(obj *Stroke) enums.EnumSetter { return &(obj.Cap) }), "stroke-linejoin": styleprops.Enum(ppath.JoinMiter, func(obj *Stroke) enums.EnumSetter { return &(obj.Join) }), "stroke-miterlimit": styleprops.Float(float32(1), func(obj *Stroke) *float32 { return &(obj.MiterLimit) }), } //////// Fill // styleFillFuncs are functions for styling the Fill object var styleFillFuncs = map[string]styleprops.Func{ "fill": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Fill) if inh, init := styleprops.InhInit(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": styleprops.Float(float32(1), func(obj *Fill) *float32 { return &(obj.Opacity) }), "fill-rule": styleprops.Enum(ppath.NonZero, func(obj *Fill) enums.EnumSetter { return &(obj.Rule) }), } //////// Paint // stylePathFuncs are functions for styling the Stroke object var stylePathFuncs = map[string]styleprops.Func{ "vector-effect": styleprops.Enum(ppath.VectorEffectNone, func(obj *Path) enums.EnumSetter { return &(obj.VectorEffect) }), "opacity": styleprops.Float(float32(1), func(obj *Path) *float32 { return &(obj.Opacity) }), "transform": func(obj any, key string, val any, parent any, cc colors.Context) { pc := obj.(*Path) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { pc.Transform = parent.(*Path).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 } //////// ToProperties // toProperties sets map[string]any properties based on non-default style values. // properties map must be non-nil. func (pc *Path) toProperties(p map[string]any) { if !pc.Display { p["display"] = "none" return } pc.Stroke.toProperties(p) pc.Fill.toProperties(p) } // toProperties sets map[string]any properties based on non-default style values. // properties map must be non-nil. func (pc *Paint) toProperties(p map[string]any) { pc.Path.toProperties(p) if !pc.Display { return } } // toProperties sets map[string]any properties based on non-default style values. // properties map must be non-nil. func (pc *Stroke) toProperties(p map[string]any) { if pc.Color == nil { p["stroke"] = "none" return } // todo: gradients! p["stroke"] = colors.AsHex(colors.ToUniform(pc.Color)) if pc.Opacity != 1 { p["stroke-opacity"] = reflectx.ToString(pc.Opacity) } if pc.Width.Unit != units.UnitDp || pc.Width.Value != 1 { p["stroke-width"] = pc.Width.StringCSS() } if pc.MinWidth.Unit != units.UnitDp || pc.MinWidth.Value != 1 { p["stroke-min-width"] = pc.MinWidth.StringCSS() } // todo: dashes if pc.Cap != ppath.CapButt { p["stroke-linecap"] = pc.Cap.String() } if pc.Join != ppath.JoinMiter { p["stroke-linecap"] = pc.Cap.String() } } // toProperties sets map[string]any properties based on non-default style values. // properties map must be non-nil. func (pc *Fill) toProperties(p map[string]any) { if pc.Color == nil { p["fill"] = "none" return } p["fill"] = colors.AsHex(colors.ToUniform(pc.Color)) if pc.Opacity != 1 { p["fill-opacity"] = reflectx.ToString(pc.Opacity) } if pc.Rule != ppath.NonZero { p["fill-rule"] = pc.Rule.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 styles import ( "image" "image/color" "cogentcore.org/core/colors" "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" "cogentcore.org/core/styles/units" ) // Path provides the styling parameters for path-level rendering: // Stroke and Fill. type Path struct { //types:add // Off indicates that node and everything below it are off, non-rendering. // This is auto-updated based on other settings. Off bool // Display is the user-settable flag that determines if this item // should be displayed. Display bool // Stroke (line drawing) parameters. Stroke Stroke // Fill (region filling) parameters. Fill Fill // Opacity is a global transparency alpha factor that applies to stroke and fill. Opacity float32 // Transform has our additions to the transform stack. Transform math32.Matrix2 // VectorEffect has various rendering special effects settings. VectorEffect ppath.VectorEffects // UnitContext has parameters necessary for determining unit sizes. UnitContext units.Context `display:"-"` // StyleSet indicates if the styles already been set. StyleSet bool `display:"-"` PropertiesNil bool `display:"-"` dotsSet bool lastUnCtxt units.Context } func (pc *Path) Defaults() { pc.Off = false pc.Display = true pc.Stroke.Defaults() pc.Fill.Defaults() pc.Opacity = 1 pc.Transform = math32.Identity2() pc.StyleSet = false } // CopyStyleFrom copies styles from another paint func (pc *Path) CopyStyleFrom(cp *Path) { pc.Off = cp.Off pc.UnitContext = cp.UnitContext pc.Stroke = cp.Stroke pc.Fill = cp.Fill pc.VectorEffect = cp.VectorEffect } // SetProperties sets path 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 *Path) SetProperties(parent *Path, properties map[string]any, ctxt colors.Context) { pc.fromProperties(parent, properties, ctxt) pc.PropertiesNil = (len(properties) == 0) pc.StyleSet = true } // GetProperties gets properties values from current style settings, // for any non-default settings, setting name-value pairs in given map, // which must be non-nil. func (pc *Path) GetProperties(properties map[string]any) { pc.toProperties(properties) } func (pc *Path) FromStyle(st *Style) { pc.UnitContext = st.UnitContext } // ToDotsImpl runs ToDots on unit values, to compile down to raw pixels func (pc *Path) ToDotsImpl(uc *units.Context) { pc.Stroke.ToDots(uc) pc.Fill.ToDots(uc) } func (pc *Path) HasFill() bool { return !pc.Off && pc.Fill.Color != nil && pc.Fill.Opacity > 0 } func (pc *Path) HasStroke() bool { return !pc.Off && pc.Stroke.Color != nil && pc.Stroke.Width.Dots > 0 && pc.Stroke.Opacity > 0 } //////// Stroke and Fill Styles // IMPORTANT: any changes here must be updated in StyleFillFuncs // Fill contains all the properties for filling a region. type Fill struct { // Color to use in filling; filling is off if nil. Color image.Image // Fill alpha opacity / transparency factor between 0 and 1. // This applies in addition to any alpha specified in the Color. Opacity float32 // Rule for how to fill more complex shapes with crossing lines. Rule ppath.FillRules } // Defaults initializes default values for paint fill func (pf *Fill) Defaults() { pf.Color = colors.Uniform(color.Black) pf.Rule = ppath.NonZero pf.Opacity = 1.0 } // ToDots runs ToDots on unit values, to compile down to raw pixels func (fs *Fill) ToDots(uc *units.Context) { } //////// Stroke // 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 // MinWidth is the minimum line width used for rendering. // If width is > 0, then this is the smallest line width. // This value is NOT subject to transforms so is in absolute // dot values, and is ignored if vector-effects, non-scaling-stroke // is used. This is an extension of the SVG / CSS standard MinWidth units.Value // Dashes are the dashes of the stroke. Each pair of values specifies // the amount to paint and then the amount to skip. Dashes []float32 // DashOffset is the starting offset for the dashes. DashOffset float32 // Cap specifies how to draw the end cap of stroked lines. Cap ppath.Caps // Join specifies how to join line segments. Join ppath.Joins // MiterLimit is the 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 = ppath.CapButt ss.Join = ppath.JoinMiter 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 = ppath.CapRound case BorderDashed: ss.Dashes = []float32{8, 6} } } // Code generated by "core generate"; DO NOT EDIT. package sides import ( "cogentcore.org/core/enums" ) var _IndexesValues = []Indexes{0, 1, 2, 3} // IndexesN is the highest valid value for type Indexes, plus one. const IndexesN Indexes = 4 var _IndexesValueMap = map[string]Indexes{`Top`: 0, `Right`: 1, `Bottom`: 2, `Left`: 3} var _IndexesDescMap = map[Indexes]string{0: ``, 1: ``, 2: ``, 3: ``} var _IndexesMap = map[Indexes]string{0: `Top`, 1: `Right`, 2: `Bottom`, 3: `Left`} // String returns the string representation of this Indexes value. func (i Indexes) String() string { return enums.String(i, _IndexesMap) } // SetString sets the Indexes value from its string representation, // and returns an error if the string is invalid. func (i *Indexes) SetString(s string) error { return enums.SetString(i, s, _IndexesValueMap, "Indexes") } // Int64 returns the Indexes value as an int64. func (i Indexes) Int64() int64 { return int64(i) } // SetInt64 sets the Indexes value from an int64. func (i *Indexes) SetInt64(in int64) { *i = Indexes(in) } // Desc returns the description of the Indexes value. func (i Indexes) Desc() string { return enums.Desc(i, _IndexesDescMap) } // IndexesValues returns all possible values for the type Indexes. func IndexesValues() []Indexes { return _IndexesValues } // Values returns all possible values for the type Indexes. func (i Indexes) Values() []enums.Enum { return enums.Values(_IndexesValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Indexes) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Indexes) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Indexes") } // 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 sides provides flexible representation of box sides // or corners, with either a single value for all, or different values // for subsets. package sides //go:generate core generate 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" ) // Indexes provides names for the Sides in order defined type Indexes int32 //enums:enum const ( Top Indexes = 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 } // AreSame returns whether all of the sides/corners are the same func AreSame[T comparable](s Sides[T]) bool { return s.Right == s.Top && s.Bottom == s.Top && s.Left == s.Top } // AreZero returns whether all of the sides/corners are equal to zero func AreZero[T comparable](s Sides[T]) bool { var zv T return s.Top == zv && s.Right == zv && s.Bottom == zv && s.Left == zv } // Values contains units.Value values for each side/corner of a box type Values struct { //types:add Sides[units.Value] } // NewValues is a helper that creates new side/corner values // and calls Set on them with the given values. func NewValues(vals ...units.Value) Values { sides := Sides[units.Value]{} sides.Set(vals...) return Values{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 Floats. func (sv *Values) ToDots(uc *units.Context) Floats { return NewFloats( 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 Floats. // It does not compute them; see ToDots for that. func (sv Values) Dots() Floats { return NewFloats( sv.Top.Dots, sv.Right.Dots, sv.Bottom.Dots, sv.Left.Dots, ) } // Floats contains float32 values for each side/corner of a box type Floats struct { //types:add Sides[float32] } // NewFloats is a helper that creates new side/corner floats // and calls Set on them with the given values. func NewFloats(vals ...float32) Floats { sides := Sides[float32]{} sides.Set(vals...) return Floats{sides} } // Add adds the side floats to the // other side floats and returns the result func (sf Floats) Add(other Floats) Floats { return NewFloats( 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 Floats) Sub(other Floats) Floats { return NewFloats( 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 Floats) MulScalar(s float32) Floats { return NewFloats( 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 Floats) Min(other Floats) Floats { return NewFloats( 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 Floats) Max(other Floats) Floats { return NewFloats( 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 Floats) Round() Floats { return NewFloats( 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 Floats) 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 Floats) Size() math32.Vector2 { return math32.Vec2(sf.Left+sf.Right, sf.Top+sf.Bottom) } // ToValues returns the side floats a // Values composed of [units.UnitDot] values func (sf Floats) ToValues() Values { return NewValues( units.Dot(sf.Top), units.Dot(sf.Right), units.Dot(sf.Bottom), units.Dot(sf.Left), ) } // Colors contains color values for each side/corner of a box type Colors struct { //types:add Sides[color.RGBA] } // NewColors 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 NewColors(vals ...color.RGBA) Colors { sides := Sides[color.RGBA]{} sides.Set(vals...) return Colors{sides} } // SetAny sets the sides/corners from the given value of any type func (s *Colors) 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 *Colors) 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("(Colors).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, 14} // StatesN is the highest valid value for type States, plus one. const StatesN States = 15 var _StatesValueMap = map[string]States{`Invisible`: 0, `Disabled`: 1, `ReadOnly`: 2, `Selected`: 3, `Active`: 4, `Dragging`: 5, `Sliding`: 6, `Focused`: 7, `Attended`: 8, `Checked`: 9, `Indeterminate`: 10, `Hovered`: 11, `LongHovered`: 12, `LongPressed`: 13, `DragHovered`: 14} var _StatesDescMap = map[States]string{0: `Invisible elements are not displayable, and thus do not present a target for GUI events. It is identical to CSS display:none. It is often used for elements such as tabs to hide elements in tabs that are not open. Elements can be made visible by toggling this flag and thus in general should be constructed and styled, but a new layout step must generally be taken after visibility status has changed. See also [cogentcore.org/core/core.WidgetBase.IsDisplayable].`, 1: `Disabled elements cannot be interacted with or selected, but do display.`, 2: `ReadOnly elements cannot be changed, but can be selected. A text input must not be ReadOnly for entering text. A button can be pressed while ReadOnly -- if not ReadOnly then the label on the button can be edited, for example.`, 3: `Selected elements have been marked for clipboard or other such actions.`, 4: `Active elements are currently being interacted with, usually involving a mouse button being pressed in the element. A text field will be active while being clicked on, and this can also result in a [Focused] state. If further movement happens, an element can also end up being Dragged or Sliding.`, 5: `Dragging means this element is currently being dragged by the mouse (i.e., a MouseDown event followed by MouseMove), as part of a drag-n-drop sequence.`, 6: `Sliding means this element is currently being manipulated via mouse to change the slider state, which will continue until the mouse is released, even if it goes off the element. It should also still be [Active].`, 7: `Focused elements receive keyboard input. Only one element can be Focused at a time.`, 8: `Attended elements are the last Activatable elements to be clicked on. Only one element can be Attended at a time. The main effect of Attended is on scrolling events: see [abilities.ScrollableUnattended]`, 9: `Checked is for check boxes or radio buttons or other similar state.`, 10: `Indeterminate indicates that the true state of an item is unknown. For example, [Checked] state items may be in an uncertain state if they represent other checked items, some of which are checked and some of which are not.`, 11: `Hovered indicates that a mouse pointer has entered the space over an element, but it is not [Active] (nor [DragHovered]).`, 12: `LongHovered indicates a Hover event that persists without significant movement for a minimum period of time (e.g., 500 msec), which typically triggers a tooltip popup.`, 13: `LongPressed indicates a MouseDown event that persists without significant movement for a minimum period of time (e.g., 500 msec), which typically triggers a tooltip and/or context menu popup.`, 14: `DragHovered indicates that a mouse pointer has entered the space over an element during a drag-n-drop sequence. This makes it a candidate for a potential drop target.`} var _StatesMap = map[States]string{0: `Invisible`, 1: `Disabled`, 2: `ReadOnly`, 3: `Selected`, 4: `Active`, 5: `Dragging`, 6: `Sliding`, 7: `Focused`, 8: `Attended`, 9: `Checked`, 10: `Indeterminate`, 11: `Hovered`, 12: `LongHovered`, 13: `LongPressed`, 14: `DragHovered`} // 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 displayable, and thus do not present // a target for GUI events. It is identical to CSS display:none. // It is often used for elements such as tabs to hide elements in // tabs that are not open. Elements can be made visible by toggling // this flag and thus in general should be constructed and styled, // but a new layout step must generally be taken after visibility // status has changed. See also [cogentcore.org/core/core.WidgetBase.IsDisplayable]. 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 // The current Focused element receives keyboard input. // Only one element can be Focused at a time. Focused // Attended is the last Pressable element to be clicked on. // Only one element can be Attended at a time. // The main effect of Attended is on scrolling events: // see [abilities.ScrollableUnattended] Attended // 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 ( "image" "image/color" "log/slog" "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/sides" "cogentcore.org/core/styles/states" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/text" ) // 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 sides.Values `display:"inline"` // Margin is the outer-most transparent space around box element, // which is _excluded_ from standard box rendering. Margin sides.Values `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 applicable to individual spans of text. Font Font // Text styling parameters applicable to a paragraph of text. 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 // 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() sides.Floats { 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() sides.Floats { mbw := s.MaxBorder.Width.Dots() if sides.AreZero(mbw.Sides) { mbw = s.Border.Width.Dots() } mbo := s.MaxBorder.Offset.Dots() if sides.AreZero(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 sides.AreZero(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 = text.Center s.Text.AlignV = text.Center } // 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 = text.WrapAsNeeded s.GrowWrap = true } else { s.Text.WhiteSpace = text.WrapNever 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, abilities.TripleClickable, abilities.Slideable) 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 ( "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/styles/styleprops" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" ) // These functions set styles from map[string]any which are used for styling // FromProperty sets style field values based on the given property key and value func (s *Style) FromProperty(parent *Style, key string, val any, cc colors.Context) { var pfont *Font var ptext *Text if parent != nil { pfont = &parent.Font ptext = &parent.Text } s.Font.FromProperty(pfont, key, val, cc) s.Text.FromProperty(ptext, key, val, cc) 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 := 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]styleprops.Func{ "color": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Style) if inh, init := styleprops.InhInit(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 := styleprops.InhInit(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": styleprops.Float(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]styleprops.Func{ "display": styleprops.Enum(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 := styleprops.InhInit(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": styleprops.Float(0, func(obj *Style) *float32 { return &obj.Grow.Y }), "wrap": styleprops.Bool(false, func(obj *Style) *bool { return &obj.Wrap }), "justify-content": styleprops.Enum(Start, func(obj *Style) enums.EnumSetter { return &obj.Justify.Content }), "justify-items": styleprops.Enum(Start, func(obj *Style) enums.EnumSetter { return &obj.Justify.Items }), "justify-self": styleprops.Enum(Auto, func(obj *Style) enums.EnumSetter { return &obj.Justify.Self }), "align-content": styleprops.Enum(Start, func(obj *Style) enums.EnumSetter { return &obj.Align.Content }), "align-items": styleprops.Enum(Start, func(obj *Style) enums.EnumSetter { return &obj.Align.Items }), "align-self": styleprops.Enum(Auto, func(obj *Style) enums.EnumSetter { return &obj.Align.Self }), "x": styleprops.Units(units.Value{}, func(obj *Style) *units.Value { return &obj.Pos.X }), "y": styleprops.Units(units.Value{}, func(obj *Style) *units.Value { return &obj.Pos.Y }), "width": styleprops.Units(units.Value{}, func(obj *Style) *units.Value { return &obj.Min.X }), "height": styleprops.Units(units.Value{}, func(obj *Style) *units.Value { return &obj.Min.Y }), "max-width": styleprops.Units(units.Value{}, func(obj *Style) *units.Value { return &obj.Max.X }), "max-height": styleprops.Units(units.Value{}, func(obj *Style) *units.Value { return &obj.Max.Y }), "min-width": styleprops.Units(units.Dp(2), func(obj *Style) *units.Value { return &obj.Min.X }), "min-height": styleprops.Units(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 := styleprops.InhInit(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 := styleprops.InhInit(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": styleprops.Enum(OverflowAuto, func(obj *Style) enums.EnumSetter { return &obj.Overflow.Y }), "columns": styleprops.Int(int(0), func(obj *Style) *int { return &obj.Columns }), "row": styleprops.Int(int(0), func(obj *Style) *int { return &obj.Row }), "col": styleprops.Int(int(0), func(obj *Style) *int { return &obj.Col }), "row-span": styleprops.Int(int(0), func(obj *Style) *int { return &obj.RowSpan }), "col-span": styleprops.Int(int(0), func(obj *Style) *int { return &obj.ColSpan }), "z-index": styleprops.Int(int(0), func(obj *Style) *int { return &obj.ZIndex }), "scrollbar-width": styleprops.Units(units.Value{}, func(obj *Style) *units.Value { return &obj.ScrollbarWidth }), } //////// Border // styleBorderFuncs are functions for styling the Border object var styleBorderFuncs = map[string]styleprops.Func{ // 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 := styleprops.InhInit(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 { styleprops.SetError(key, val, err) } } }, "border-width": func(obj any, key string, val any, parent any, cc colors.Context) { bs := obj.(*Border) if inh, init := styleprops.InhInit(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 := styleprops.InhInit(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 := styleprops.InhInit(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]styleprops.Func{ "outline-style": func(obj any, key string, val any, parent any, cc colors.Context) { bs := obj.(*Border) if inh, init := styleprops.InhInit(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 { styleprops.SetError(key, val, err) } } }, "outline-width": func(obj any, key string, val any, parent any, cc colors.Context) { bs := obj.(*Border) if inh, init := styleprops.InhInit(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 := styleprops.InhInit(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 := styleprops.InhInit(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]styleprops.Func{ "box-shadow.offset-x": styleprops.Units(units.Value{}, func(obj *Shadow) *units.Value { return &obj.OffsetX }), "box-shadow.offset-y": styleprops.Units(units.Value{}, func(obj *Shadow) *units.Value { return &obj.OffsetY }), "box-shadow.blur": styleprops.Units(units.Value{}, func(obj *Shadow) *units.Value { return &obj.Blur }), "box-shadow.spread": styleprops.Units(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 := styleprops.InhInit(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": styleprops.Bool(false, func(obj *Shadow) *bool { return &obj.Inset }), } //////// Font // FromProperties sets style field values based on the given property list. func (s *Font) FromProperties(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 } s.FromProperty(parent, key, val, ctxt) } } // FromProperty sets style field values based on the given property key and value. func (s *Font) FromProperty(parent *Font, key string, val any, cc colors.Context) { if sfunc, ok := styleFontFuncs[key]; ok { if parent != nil { sfunc(s, key, val, parent, cc) } else { sfunc(s, key, val, nil, cc) } return } } // 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, } // styleFontFuncs are functions for styling the Font object. var styleFontFuncs = map[string]styleprops.Func{ // note: text.Style handles the standard units-based font-size settings "font-size": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Font) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.Size = parent.(*Font).Size } else if init { fs.Size.Set(16, units.UnitDp) } 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 } } }, "font-family": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Font) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.Family = parent.(*Font).Family } else if init { fs.Family = rich.SansSerif // font has defaults } return } switch vt := val.(type) { case string: fs.CustomFont = rich.FontName(vt) fs.Family = rich.Custom default: // todo: process enum } }, "font-style": styleprops.Enum(rich.SlantNormal, func(obj *Font) enums.EnumSetter { return &obj.Slant }), "font-weight": styleprops.Enum(rich.Normal, func(obj *Font) enums.EnumSetter { return &obj.Weight }), "font-stretch": styleprops.Enum(rich.StretchNormal, func(obj *Font) enums.EnumSetter { return &obj.Stretch }), "text-decoration": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Font) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.Decoration = parent.(*Font).Decoration } else if init { fs.Decoration = 0 } return } switch vt := val.(type) { case string: if vt == "none" { fs.Decoration = 0 } else { fs.Decoration.SetString(vt) } case rich.Decorations: fs.Decoration.SetFlag(true, vt) default: iv, err := reflectx.ToInt(val) if err == nil { fs.Decoration.SetFlag(true, rich.Decorations(iv)) } else { styleprops.SetError(key, val, err) } } }, } // FromProperties sets style field values based on the given property list. func (s *Text) FromProperties(parent *Text, 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 } s.FromProperty(parent, key, val, ctxt) } } // FromProperty sets style field values based on the given property key and value. func (s *Text) FromProperty(parent *Text, key string, val any, cc colors.Context) { if sfunc, ok := styleFuncs[key]; ok { if parent != nil { sfunc(s, key, val, parent, cc) } else { sfunc(s, key, val, nil, cc) } return } } // styleFuncs are functions for styling the Text object. var styleFuncs = map[string]styleprops.Func{ "text-align": styleprops.Enum(Start, func(obj *Text) enums.EnumSetter { return &obj.Align }), "text-vertical-align": styleprops.Enum(Start, func(obj *Text) enums.EnumSetter { return &obj.AlignV }), "line-height": styleprops.FloatProportion(float32(1.2), func(obj *Text) *float32 { return &obj.LineHeight }), "line-spacing": styleprops.FloatProportion(float32(1.2), func(obj *Text) *float32 { return &obj.LineHeight }), "white-space": styleprops.Enum(text.WrapAsNeeded, func(obj *Text) enums.EnumSetter { return &obj.WhiteSpace }), "direction": styleprops.Enum(rich.LTR, func(obj *Text) enums.EnumSetter { return &obj.Direction }), "tab-size": styleprops.Int(int(4), func(obj *Text) *int { return &obj.TabSize }), "select-color": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Text) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.SelectColor = parent.(*Text).SelectColor } else if init { fs.SelectColor = colors.Scheme.Select.Container } return } fs.SelectColor = errors.Log1(gradient.FromAny(val, cc)) }, "highlight-color": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Text) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.HighlightColor = parent.(*Text).HighlightColor } else if init { fs.HighlightColor = colors.Scheme.Warn.Container } return } fs.HighlightColor = errors.Log1(gradient.FromAny(val, cc)) }, } // 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 styleprops provides infrastructure for property-list-based setting // of style values, where a property list is a map[string]any collection of // key, value pairs. package styleprops import ( "log/slog" "reflect" "strings" "cogentcore.org/core/base/num" "cogentcore.org/core/base/reflectx" "cogentcore.org/core/colors" "cogentcore.org/core/enums" "cogentcore.org/core/styles/units" ) // Func is the signature for styleprops functions type Func func(obj any, key string, val any, parent any, cc colors.Context) // InhInit detects the style values of "inherit" and "initial", // setting the corresponding bool return values func InhInit(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 } // FuncInt returns a style function for any numerical value func Int[T any, F num.Integer](initVal F, getField func(obj *T) *F) Func { return func(obj any, key string, val any, parent any, cc colors.Context) { fp := getField(obj.(*T)) if inh, init := InhInit(val, parent); inh || init { if inh { *fp = *getField(parent.(*T)) } else if init { *fp = initVal } return } fv, _ := reflectx.ToInt(val) *fp = F(fv) } } // Float returns a style function for any numerical value. // Automatically removes a trailing % -- see FloatProportion. func Float[T any, F num.Float](initVal F, getField func(obj *T) *F) Func { return func(obj any, key string, val any, parent any, cc colors.Context) { fp := getField(obj.(*T)) if inh, init := InhInit(val, parent); inh || init { if inh { *fp = *getField(parent.(*T)) } else if init { *fp = initVal } return } if vstr, ok := val.(string); ok { val = strings.TrimSuffix(vstr, "%") } fv, _ := reflectx.ToFloat(val) // can represent any number, ToFloat is fast type switch *fp = F(fv) } } // FloatProportion returns a style function for a proportion that can be // represented as a percentage (divides value by 100). func FloatProportion[T any, F num.Float](initVal F, getField func(obj *T) *F) Func { return func(obj any, key string, val any, parent any, cc colors.Context) { fp := getField(obj.(*T)) if inh, init := InhInit(val, parent); inh || init { if inh { *fp = *getField(parent.(*T)) } else if init { *fp = initVal } return } isPct := false if vstr, ok := val.(string); ok { val = strings.TrimSuffix(vstr, "%") isPct = true } fv, _ := reflectx.ToFloat(val) // can represent any number, ToFloat is fast type switch if isPct { fv /= 100 } *fp = F(fv) } } // Bool returns a style function for a bool value func Bool[T any](initVal bool, getField func(obj *T) *bool) Func { return func(obj any, key string, val any, parent any, cc colors.Context) { fp := getField(obj.(*T)) if inh, init := InhInit(val, parent); inh || init { if inh { *fp = *getField(parent.(*T)) } else if init { *fp = initVal } return } fv, _ := reflectx.ToBool(val) *fp = fv } } // Units returns a style function for units.Value func Units[T any](initVal units.Value, getField func(obj *T) *units.Value) Func { return func(obj any, key string, val any, parent any, cc colors.Context) { fp := getField(obj.(*T)) if inh, init := InhInit(val, parent); inh || init { if inh { *fp = *getField(parent.(*T)) } else if init { *fp = initVal } return } fp.SetAny(val, key) } } // Enum returns a style function for any enum value func Enum[T any](initVal enums.Enum, getField func(obj *T) enums.EnumSetter) Func { return func(obj any, key string, val any, parent any, cc colors.Context) { fp := getField(obj.(*T)) if inh, init := InhInit(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)) } } // SetError reports that cannot set property of given key with given value due to given error func SetError(key string, val any, err error) { slog.Error("styleprops: error setting value", "key", key, "value", val, "err", 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 styleprops import ( "fmt" "strings" "cogentcore.org/core/base/reflectx" ) // FromXMLString sets style properties from XML style string, which contains ';' // separated name: value pairs func FromXMLString(style string, properties map[string]any) { st := strings.Split(style, ";") for _, s := range st { kv := strings.Split(s, ":") if len(kv) >= 2 { k := strings.TrimSpace(strings.ToLower(kv[0])) v := strings.TrimSpace(kv[1]) properties[k] = v } } } // ToXMLString returns an XML style string from given style properties map // using ';' separated name: value pairs. func ToXMLString(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() } // 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/math32" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" ) // Text has styles for text layout styling. // Most of these are inherited type Text struct { //types:add // Align specifies how to align text along the default direction (inherited). // This *only* applies to the text within its containing element, // and is relevant only for multi-line text. Align text.Aligns // AlignV specifies "vertical" (orthogonal to default direction) // 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 text.Aligns // LineHeight is a multiplier on the default font size for spacing between lines. // If there are larger font elements within a line, they will be accommodated, with // the same amount of total spacing added above that maximum size as if it was all // the same height. The default of 1.3 represents standard "single spaced" text. LineHeight float32 `default:"1.3"` // 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 text.WhiteSpaces // Direction specifies the default text direction, which can be overridden if the // unicode text is typically written in a different direction. Direction rich.Directions // TabSize specifies the tab size, in number of characters (inherited). TabSize int // SelectColor is the color to use for the background region of selected text (inherited). SelectColor image.Image // HighlightColor is the color to use for the background region of highlighted text (inherited). HighlightColor image.Image } func (ts *Text) Defaults() { ts.Align = text.Start ts.AlignV = text.Start ts.LineHeight = 1.3 ts.Direction = rich.LTR ts.TabSize = 4 ts.SelectColor = colors.Scheme.Select.Container ts.HighlightColor = colors.Scheme.Warn.Container } // ToDots runs ToDots on unit values, to compile down to raw pixels func (ts *Text) ToDots(uc *units.Context) { } // InheritFields from parent func (ts *Text) InheritFields(parent *Text) { ts.Align = parent.Align ts.AlignV = parent.AlignV ts.LineHeight = parent.LineHeight // ts.WhiteSpace = par.WhiteSpace // note: we can't inherit this b/c label base default then gets overwritten ts.Direction = parent.Direction ts.TabSize = parent.TabSize ts.SelectColor = parent.SelectColor ts.HighlightColor = parent.HighlightColor } // SetText sets the text.Style from this style. func (ts *Text) SetText(tsty *text.Style) { tsty.Align = ts.Align tsty.AlignV = ts.AlignV tsty.LineHeight = ts.LineHeight tsty.WhiteSpace = ts.WhiteSpace tsty.Direction = ts.Direction tsty.TabSize = ts.TabSize tsty.SelectColor = ts.SelectColor tsty.HighlightColor = ts.HighlightColor } // LineHeightDots returns the effective line height in dots (actual pixels) // as FontHeight * LineHeight func (s *Style) LineHeightDots() float32 { return math32.Ceil(s.Font.FontHeight() * s.Text.LineHeight) } // 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" "cogentcore.org/core/math32" ) // 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 font based on the em size, // which is the nominal font height, in DPI dots. // This uses standard conversion factors from em. It is too unreliable // and complicated to get these values from the actual font itself. func (uc *Context) SetFont(em float32) { if em == 0 { em = 16 } uc.FontEm = em uc.FontEx = math32.Round(0.53 * em) uc.FontCh = math32.Round(0.45 * em) uc.FontRem = math32.Round(uc.Dp(16)) } // 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(sv *SVG) 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) { if !g.IsVisible(sv) { return } pc := g.Painter(sv) pc.Circle(g.Pos.X, g.Pos.Y, g.Radius) pc.Draw() g.RenderChildren(sv) } // 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/math32" "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(math32.Vec2(float32(c.Render.Width), float32(c.Render.Height))) err := ApplyFill(c, sv) if err != nil { return err } err = sv.OpenXML(c.Input) if err != nil { return err } if c.Output == "" { c.Output = strings.TrimSuffix(c.Input, filepath.Ext(c.Input)) + ".png" } return sv.SaveImage(c.Output) } // EmbedImage embeds the input image file into the output svg file. func EmbedImage(c *Config) error { sv := svg.NewSVG(math32.Vec2(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 } sz := img.Pixels.Bounds().Size() sv.Root.ViewBox.Size.SetPoint(sz) 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(sv *SVG) 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) { if !g.IsVisible(sv) { return } pc := g.Painter(sv) pc.Ellipse(g.Pos.X, g.Pos.Y, g.Radii.X, g.Radii.Y) pc.Draw() } // 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)) 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(sv)) gb.SetTransform(math32.Identity2()) } // GradientCloneNodeProp creates a new clone of the existing gradient for node // if set for given property key ("fill" or "stroke"). // returns new gradient. func (sv *SVG) GradientCloneNodeProp(n Node, prop string) *Gradient { ps := n.AsTree().Property(prop) if ps == nil { return nil } pstr := ps.(string) radial := false if strings.HasPrefix(pstr, "url(#radialGradient") { radial = true } else if !strings.HasPrefix(pstr, "url(#linearGradient") { return nil } gr := sv.GradientByName(n, pstr) if gr == nil { return nil } ngr, url := sv.GradientNewForNode(n, radial, gr.StopsName) n.AsTree().SetProperty(prop, url) gradient.CopyFrom(ngr.Grad, gr.Grad) // TODO(kai): should this return ngr or gr? (used to return gr but ngr seems correct) return ngr } // GradientDeleteNodeProp deletes any existing gradient for node // if set for given property key ("fill" or "stroke"). // Returns true if deleted. func (sv *SVG) GradientDeleteNodeProp(n Node, prop string) bool { ps := n.AsTree().Property(prop) if ps == nil { return false } pstr := ps.(string) if !strings.HasPrefix(pstr, "url(#radialGradient") && !strings.HasPrefix(pstr, "url(#linearGradient") { return false } return sv.GradientDeleteForNode(n, pstr) } // GradientUpdateAllStops removes any items from Defs that are not actually referred to // by anything in the current SVG tree. Returns true if items were removed. // Does not remove gradients with StopsName = "" with extant stops -- these // 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 ( "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 } func (g *Group) BBoxes(sv *SVG, parTransform math32.Matrix2) { g.BBoxesFromChildren(sv, parTransform) } func (g *Group) Render(sv *SVG) { if !g.PushContext(sv) { return } pc := g.Painter(sv) g.RenderChildren(sv) pc.PopContext() } // 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" "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"` // Pixels are the image pixels, which has imagex.WrapJS already applied. Pixels image.Image `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 } // pixelsOfSize returns the Pixels as an imagex.Image of given size. // makes a new one if not already the correct size. func (g *Image) pixelsOfSize(nwsz image.Point) image.Image { if nwsz.X == 0 || nwsz.Y == 0 { return nil } if g.Pixels != nil && g.Pixels.Bounds().Size() == nwsz { return g.Pixels } g.Pixels = imagex.WrapJS(image.NewRGBA(image.Rectangle{Max: nwsz})) return g.Pixels } // 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) { if img == nil { return } img = imagex.Unwrap(img) sz := img.Bounds().Size() if width <= 0 && height <= 0 { cp := imagex.CloneAsRGBA(img) g.Pixels = imagex.WrapJS(cp) 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) } pxi := g.pixelsOfSize(tsz) px := imagex.Unwrap(pxi).(*image.RGBA) 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(px, 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 := g.Painter(sv) pc.DrawImageScaled(g.Pixels, g.Pos.X, g.Pos.Y, g.Size.X, g.Size.Y) } func (g *Image) LocalBBox(sv *SVG) math32.Box2 { bb := math32.Box2{} bb.Min = g.Pos bb.Max = g.Pos.Add(g.Size) return bb.Canon() } func (g *Image) Render(sv *SVG) { vis := g.IsVisible(sv) if !vis { return } g.DrawImage(sv) g.RenderChildren(sv) } // 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/base/stack" "cogentcore.org/core/colors" "cogentcore.org/core/colors/gradient" "cogentcore.org/core/math32" "cogentcore.org/core/styles/styleprops" "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 var groupStack stack.Stack[string] 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 if nm == "g" { name := "" if sv.GroupFilter != "" { for _, attr := range se.Attr { if attr.Name.Local != "id" { continue } name = attr.Value if name != sv.GroupFilter { sv.groupFilterSkip = true sv.groupFilterSkipName = name // fmt.Println("skipping:", attr.Value, sv.GroupFilter) break // } else { // fmt.Println("including:", attr.Value, sv.GroupFilter) } } } if name == "" { name = fmt.Sprintf("tmp%d", len(groupStack)+1) } groupStack.Push(name) if sv.groupFilterSkip { break } 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) } } break } if sv.groupFilterSkip { break } 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 == "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": if sv.GroupFilter != "" && inDef { // font optimization path.DataStr = attr.Value } else { 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 { fmt.Println("can't find use:", link) break } cln := itm.AsTree().Clone().(Node) if cln == nil { break } curPar.AsTree().AddChild(cln) var xo, yo float64 for _, attr := range se.Attr { if SetStandardXMLAttr(cln.AsNodeBase(), attr.Name.Local, attr.Value) { continue } switch attr.Name.Local { case "x": xo, _ = reflectx.ToFloat(attr.Value) case "y": yo, _ = reflectx.ToFloat(attr.Value) default: cln.AsTree().SetProperty(attr.Name.Local, attr.Value) } } if xo != 0 || yo != 0 { xf := math32.Translate2D(float32(xo), float32(yo)) if txp, has := cln.AsTree().Properties["transform"]; has { exf := math32.Identity2() exf.SetString(txp.(string)) exf = exf.Translate(float32(xo), float32(yo)) cln.AsTree().SetProperty("transform", exf.String()) } else { cln.AsTree().SetProperty("transform", xf.String()) } } if p, ok := cln.(*Path); ok { p.SetData(p.DataStr) // defs don't apply paths } 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: nm := se.Name.Local if nm == "g" { cg := groupStack.Pop() if sv.groupFilterSkip { if sv.groupFilterSkipName == cg { // fmt.Println("unskip:", cg) sv.groupFilterSkip = false } break } 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 } break } if sv.groupFilterSkip { break } switch nm { 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 := styleprops.ToXMLString(properties) if sp != "" { XMLAddAttr(&se.Attr, "style", sp) } if txp, has := properties["transform"]; has { XMLAddAttr(&se.Attr, "transform", reflectx.ToString(txp)) } } else { for k, v := range properties { sv := reflectx.ToString(v) if _, has := InkscapeProperties[k]; has { k = "inkscape:" + k } else if k == "overflow" { k = "style" sv = "overflow:" + sv } XMLAddAttr(&se.Attr, k, sv) } } } var sb strings.Builder nm := "" switch nd := n.(type) { case *Path: nm = "path" nd.DataStr = nd.Data.ToSVG() 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 { kn := k.(Node) if setName == "defs" { if _, ok := kn.(*Path); ok { // skip paths in defs b/c just for use and copied continue } } knm, err := MarshalXMLTree(kn, 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": if nb.Properties == nil { nb.Properties = make(map[string]any) } styleprops.FromXMLString(val, 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(sv *SVG) 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) { if !g.IsVisible(sv) { return } pc := g.Painter(sv) pc.Line(g.Start.X, g.Start.Y, g.End.X, g.End.Y) pc.Draw() g.PushContext(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.Stroke.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.Stroke.Width.Dots) } pc.PopContext() } // 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.EffSize, mrk.Transform) mrk.Render(sv) } func (g *Marker) BBoxes(sv *SVG, parTransform math32.Matrix2) { g.BBoxesFromChildren(sv, parTransform) } func (g *Marker) Render(sv *SVG) { pc := g.Painter(sv) pc.PushContext(&g.Paint, nil) g.RenderChildren(sv) pc.PopContext() } //////// 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 // BBoxes computes BBox and VisBBox, prior to render. BBoxes(sv *SVG, parTransform math32.Matrix2) // Render draws the node to the svg image. Render(sv *SVG) // LocalBBox returns the bounding box of node in local dimensions. LocalBBox(sv *SVG) math32.Box2 // 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(sv *SVG) 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)) } // ParentTransform 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) ParentTransform(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.ParentTransform(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.SetProperties(pp, g.Properties, ctxt) } else { pc.SetProperties(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.Stroke.Opacity *= pc.Opacity // applies to all pc.Fill.Opacity *= pc.Opacity pc.Off = (pc.Stroke.Color == nil && pc.Fill.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.SetProperties(pp, pmap, ctxt) } else { pc.SetProperties(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) } 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.Stroke.Color == nil { return 0 } return pc.Stroke.Width.Dots } func (g *NodeBase) BBoxes(sv *SVG, parTransform math32.Matrix2) { xf := parTransform.Mul(g.Paint.Transform) ni := g.This.(Node) lbb := ni.LocalBBox(sv) g.BBox = lbb.MulMatrix2(xf).ToRect() g.VisBBox = sv.Geom.SizeRect().Intersect(g.BBox) } // IsVisible checks our bounding box and visibility, returning false if // out of bounds. Must be called as first step in Render. func (g *NodeBase) IsVisible(sv *SVG) bool { if g.Paint.Off || g == nil || g.This == nil { return false } nvis := g.VisBBox == image.Rectangle{} if nvis && !g.isDef { // fmt.Println("invisible:", g.Name, g.BBox, g.VisBBox) return false } return true } // Painter returns a new Painter using my styles. func (g *NodeBase) Painter(sv *SVG) *paint.Painter { return &paint.Painter{sv.painter.State, &g.Paint} } // PushContext checks our bounding box and visibility, returning false if // out of bounds. If visible, pushes us as Context. // Must be called as first step in Render. func (g *NodeBase) PushContext(sv *SVG) bool { if !g.IsVisible(sv) { return false } pc := g.Painter(sv) pc.PushContext(&g.Paint, nil) return true } func (g *NodeBase) BBoxesFromChildren(sv *SVG, parTransform math32.Matrix2) { xf := parTransform.Mul(g.Paint.Transform) var bb image.Rectangle for i, kid := range g.Children { ni := kid.(Node) ni.BBoxes(sv, xf) nb := ni.AsNodeBase() if i == 0 { bb = nb.BBox } else { bb = bb.Union(nb.BBox) } } g.BBox = bb g.VisBBox = sv.Geom.SizeRect().Intersect(g.BBox) } func (g *NodeBase) RenderChildren(sv *SVG) { for _, kid := range g.Children { ni := kid.(Node) ni.Render(sv) } } func (g *NodeBase) Render(sv *SVG) { if !g.IsVisible(sv) { return } g.RenderChildren(sv) } // 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/paint/ppath" ) // Path renders SVG data sequences that can render just about anything type Path struct { NodeBase // Path data using paint/ppath representation. Data ppath.Path `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 = ppath.ParseSVGPath(data) if err != nil { return err } return err } func (g *Path) LocalBBox(sv *SVG) math32.Box2 { bb := g.Data.FastBounds() 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 || !g.IsVisible(sv) { return } pc := g.Painter(sv) pc.State.Path = g.Data.Clone() // note: yes this Clone() is absolutely necessary. pc.Draw() g.PushContext(sv) mrk_start := sv.MarkerByName(g, "marker-start") mrk_end := sv.MarkerByName(g, "marker-end") mrk_mid := sv.MarkerByName(g, "marker-mid") if mrk_start != nil || mrk_end != nil || mrk_mid != nil { pos := g.Data.Coords() dir := g.Data.CoordDirections() np := len(pos) if mrk_start != nil && np > 0 { ang := ppath.Angle(dir[0]) mrk_start.RenderMarker(sv, pos[0], ang, g.Paint.Stroke.Width.Dots) } if mrk_end != nil && np > 1 { ang := ppath.Angle(dir[np-1]) mrk_end.RenderMarker(sv, pos[np-1], ang, g.Paint.Stroke.Width.Dots) } if mrk_mid != nil && np > 2 { for i := 1; i < np-2; i++ { ang := ppath.Angle(dir[i]) mrk_mid.RenderMarker(sv, pos[i], ang, g.Paint.Stroke.Width.Dots) } } } pc.PopContext() } // UpdatePathString sets the path string from the Data func (g *Path) UpdatePathString() { g.DataStr = g.Data.ToSVG() } //////// Transforms // ApplyTransform applies the given 2D transform to the geometry of this node // each node must define this for itself func (g *Path) ApplyTransform(sv *SVG, xf math32.Matrix2) { // path may have horiz, vert elements -- only gen soln is to transform g.Paint.Transform.SetMul(xf) g.SetProperty("transform", g.Paint.Transform.String()) } // ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node // relative to given point. Trans translation and point are in top-level coordinates, // so must be transformed into local coords first. // Point is upper left corner of selection box that anchors the translation and scaling, // and for rotation it is the center point around which to rotate func (g *Path) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { crot := g.Paint.Transform.ExtractRot() if rot != 0 || crot != 0 { xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) // exclude self g.Paint.Transform.SetMulCenter(xf, lpt) g.SetProperty("transform", g.Paint.Transform.String()) } else { xf, lpt := g.DeltaTransform(trans, scale, rot, pt, true) // include self g.ApplyTransformImpl(xf, lpt) g.GradientApplyTransformPt(sv, xf, lpt) } } // ApplyTransformImpl does the implementation of applying a transform to all points func (g *Path) ApplyTransformImpl(xf math32.Matrix2, lpt math32.Vector2) { g.Data.Transform(xf) } // WriteGeom writes the geometry of the node to a slice of floating point numbers // the length and ordering of which is specific to each node type. // Slice must be passed and will be resized if not the correct length. func (g *Path) WriteGeom(sv *SVG, dat *[]float32) { sz := len(g.Data) *dat = slicesx.SetLength(*dat, sz+6) for i := range g.Data { (*dat)[i] = float32(g.Data[i]) } g.WriteTransform(*dat, sz) g.GradientWritePts(sv, dat) } // ReadGeom reads the geometry of the node from a slice of floating point numbers // the length and ordering of which is specific to each node type. func (g *Path) ReadGeom(sv *SVG, dat []float32) { sz := len(g.Data) g.Data = ppath.Path(dat) g.ReadTransform(dat, sz) g.GradientReadPts(sv, dat) } // 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 || !g.IsVisible(sv) { return } pc := g.Painter(sv) pc.Polygon(g.Points...) pc.Draw() g.PushContext(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.Stroke.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.Stroke.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.Stroke.Width.Dots) } } pc.PopContext() } // 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(sv *SVG) 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 || !g.IsVisible(sv) { return } pc := g.Painter(sv) pc.Polyline(g.Points...) pc.Draw() g.PushContext(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.Stroke.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.Stroke.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.Stroke.Width.Dots) } } pc.PopContext() } // 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/sides" ) // 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. only rx is used for now. 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(sv *SVG) 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) { if !g.IsVisible(sv) { return } pc := g.Painter(sv) if g.Radius.X == 0 && g.Radius.Y == 0 { pc.Rectangle(g.Pos.X, g.Pos.Y, g.Size.X, g.Size.Y) } else { // todo: only supports 1 radius right now -- easy to add another // the Painter also support different radii for each corner but not rx, ry at this point, // although that would be easy to add TODO: pc.RoundedRectangleSides(g.Pos.X, g.Pos.Y, g.Size.X, g.Size.Y, sides.NewFloats(g.Radius.X)) } pc.Draw() } // 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 ( "bytes" "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/paint/render" "cogentcore.org/core/styles" "cogentcore.org/core/styles/sides" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/shaped" "cogentcore.org/core/tree" ) var ( // svgShaper is a shared text shaper. svgShaper shaped.Shaper // mutex for initializing the svgShaper. shaperMu sync.Mutex ) // SVGToImage generates an image from given svg source, // with given width and height size. func SVGToImage(svg []byte, size math32.Vector2) (image.Image, error) { sv := NewSVG(size) err := sv.ReadXML(bytes.NewBuffer(svg)) return sv.RenderImage(), err } // SVG represents a structured SVG vector graphics drawing, // with nodes allocated for each element. // It renders to a [paint.Painter] via the Render method. // Any supported representation can then be rendered from that. 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 RenderImage 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 // painter is the current painter being used, which is only valid during rendering. painter *paint.Painter // TextShaper for shaping text. Can set to a shared external one, // or else the shared svgShaper is used. TextShaper shaped.Shaper // 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 // GroupFilter is used to filter group names, skipping any that don't contain // this string, if non-empty. This is needed e.g., for reading SVG font files // which pack many elements into the same file. GroupFilter string // groupFilterSkip is whether to skip the current group based on GroupFilter. groupFilterSkip bool // groupFilterSkipName is name of group currently skipping. groupFilterSkipName string // 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:"-"` // mutex for protecting rendering sync.Mutex } // NewSVG creates a SVG with the given viewport size, // which is typically in pixel dots. func NewSVG(size math32.Vector2) *SVG { sv := &SVG{} sv.Init(size) return sv } // Init initializes the SVG with given viewport size, // which is typically in pixel dots. func (sv *SVG) Init(size math32.Vector2) { sv.Geom.Size = size.ToPointCeil() sv.Scale = 1 sv.Root = NewRoot() sv.Root.SetName("svg") sv.Defs = NewGroup() sv.Defs.SetName("defs") sv.SetUnitContext(&sv.Root.Paint) } // SetSize updates the viewport size. func (sv *SVG) SetSize(size math32.Vector2) { sv.Geom.Size = size.ToPointCeil() sv.SetUnitContext(&sv.Root.Paint) } // 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) sv.Root.WalkDown(func(k tree.Node) bool { sn := k.(Node) sn.AsNodeBase().Style(sv) return tree.Continue }) } // Render renders the SVG to given Painter, which can be nil // to have a new one created. Returns the painter used. // Set the TextShaper prior to calling to use an existing one, // otherwise it will use shared svgShaper. func (sv *SVG) Render(pc *paint.Painter) *paint.Painter { sv.Lock() defer sv.Unlock() if pc != nil { sv.painter = pc } else { sv.painter = paint.NewPainter(math32.FromPoint(sv.Geom.Size)) pc = sv.painter } if sv.TextShaper == nil { shaperMu.Lock() if svgShaper == nil { svgShaper = shaped.NewShaper() } sv.TextShaper = svgShaper shaperMu.Unlock() defer func() { sv.TextShaper = nil }() } sv.Style() sv.SetRootTransform() sv.Root.BBoxes(sv, math32.Identity2()) if sv.Background != nil { sv.FillViewport() } sv.Root.Render(sv) sv.painter = nil return pc } // RenderImage renders the SVG to an image and returns it. func (sv *SVG) RenderImage() image.Image { return paint.RenderToImage(sv.Render(nil)) } // SaveImage renders the SVG to an image and saves it to given filename, // using the filename extension to determine the file type. func (sv *SVG) SaveImage(fname string) error { return imagex.Save(sv.RenderImage(), fname) } func (sv *SVG) FillViewport() { sty := styles.NewPaint() // has no transform pc := &paint.Painter{sv.painter.State, sty} 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) } // 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 } // 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) { pc.UnitContext.Defaults() pc.UnitContext.DPI = 96 // paint (SVG) context is always 96 = 1to1 wd := float32(sv.Geom.Size.X) ht := float32(sv.Geom.Size.Y) pc.UnitContext.SetSizes(wd, ht, wd, ht, wd, ht) // self, element, parent -- all same pc.ToDots() sv.ToDots(&pc.UnitContext) } func (sv *SVG) ToDots(uc *units.Context) { sv.PhysicalWidth.ToDots(uc) sv.PhysicalHeight.ToDots(uc) } func (g *Root) Render(sv *SVG) { pc := g.Painter(sv) pc.PushContext(&g.Paint, render.NewBoundsRect(sv.Geom.Bounds(), sides.NewFloats())) g.RenderChildren(sv) pc.PopContext() } // 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/colors" "cogentcore.org/core/math32" "cogentcore.org/core/text/htmltext" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/text" ) // Text renders SVG text, handling both text and tspan elements. // tspan is nested under a parent text, where 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 TextShaped *shaped.Lines `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 } 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 } } // LocalBBox does full text layout, but no transforms func (g *Text) LocalBBox(sv *SVG) math32.Box2 { if g.Text == "" { return math32.Box2{} } pc := &g.Paint fs := pc.Font if pc.Fill.Color != nil { fs.SetFillColor(colors.ToUniform(pc.Fill.Color)) } tx, _ := htmltext.HTMLToRich([]byte(g.Text), &fs, nil) // fmt.Println(tx) sz := math32.Vec2(10000, 10000) g.TextShaped = sv.TextShaper.WrapLines(tx, &fs, &pc.Text, &rich.DefaultSettings, sz) // baseOff := g.TextShaped.Lines[0].Offset g.TextShaped.StartAtBaseline() // remove top-left offset // fmt.Println("baseoff:", baseOff) // fmt.Println(pc.Text.FontSize, pc.Text.FontSize.Dots) // 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.TextShaped.UpdateBBox() return g.TextShaped.Bounds.Translate(g.Pos) } func (g *Text) BBoxes(sv *SVG, parTransform math32.Matrix2) { if g.IsParText() { g.BBoxesFromChildren(sv, parTransform) return } xf := parTransform.Mul(g.Paint.Transform) ni := g.This.(Node) lbb := ni.LocalBBox(sv) g.BBox = lbb.MulMatrix2(xf).ToRect() g.VisBBox = sv.Geom.SizeRect().Intersect(g.BBox) } func (g *Text) Render(sv *SVG) { if g.IsParText() { if !g.PushContext(sv) { return } pc := g.Painter(sv) g.RenderChildren(sv) pc.PopContext() return } if !g.IsVisible(sv) { return } if len(g.Text) > 0 { g.RenderText(sv) } } func (g *Text) RenderText(sv *SVG) { // note: transform is managed entirely in the render side function! pc := g.Painter(sv) pos := g.Pos bsz := g.TextShaped.Bounds.Size() if pc.Text.Align == text.Center { pos.X -= bsz.X * .5 } else if pc.Text.Align == text.End { pos.X -= bsz.X } pc.DrawText(g.TextShaped, pos) } // 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/text/shaped" "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: "Pixels are the image pixels, which has imagex.WrapJS already applied."}}}) // 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]: // Pixels are the image pixels, which has imagex.WrapJS already applied. func (t *Image) SetPixels(v image.Image) *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: "Path data using paint/ppath representation."}, {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. only rx is used for now."}}}) // 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. only rx is used for now. 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, where text has empty Text string.", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Pos", Doc: "position of the left, baseline of the text"}, {Name: "Width", Doc: "width of text to render if using word-wrapping"}, {Name: "Text", Doc: "text string to render"}, {Name: "TextShaped", Doc: "render version of text"}, {Name: "CharPosX", Doc: "character positions along X axis, if specified"}, {Name: "CharPosY", Doc: "character positions along Y axis, if specified"}, {Name: "CharPosDX", Doc: "character delta-positions along X axis, if specified"}, {Name: "CharPosDY", Doc: "character delta-positions along Y axis, if specified"}, {Name: "CharRots", Doc: "character rotations, if specified"}, {Name: "TextLength", Doc: "author's computed text length, if specified -- we attempt to match"}, {Name: "AdjustGlyphs", Doc: "in attempting to match TextLength, should we adjust glyphs in addition to spacing?"}}}) // NewText returns a new [Text] with the given optional parent: // Text renders SVG text, handling both text and tspan elements. // tspan is nested under a parent text, where text has empty Text string. 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 } // SetTextShaped sets the [Text.TextShaped]: // render version of text func (t *Text) SetTextShaped(v *shaped.Lines) *Text { t.TextShaped = 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 } // 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" "errors" "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 errors.New("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 errors.New("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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package csl import ( "unicode" "cogentcore.org/core/text/rich" ) // definitive reference: // https://apastyle.apa.org/style-grammar-guidelines/references/examples // CiteAPA generates a APA-style citation, as Last[ & Last|et al.] Year // with a , before Year in Parenthetical style, and Parens around the Year // in Narrative style. func CiteAPA(cs CiteStyles, it *Item) string { c := "" if len(it.Author) > 0 { c = NamesCiteEtAl(it.Author) } else { c = NamesCiteEtAl(it.Editor) } switch cs { case Parenthetical: c += ", " + it.Issued.Year() case Narrative: c += " (" + it.Issued.Year() + ")" } return c } // RefAPA generates an APA-style reference entry from the given item, // with rich.Text formatting of italics around the source, volume, // and spans for each separate chunk. // Use Join method to get full raw text. func RefAPA(it *Item) rich.Text { switch it.Type { case Book, Collection: return RefAPABook(it) case Chapter, PaperConference: return RefAPAChapter(it) case Thesis: return RefAPAThesis(it) case Article, ArticleJournal, ArticleMagazine, ArticleNewspaper: return RefAPAArticle(it) default: return RefAPAMisc(it) } } // RefsAPA generates a list of APA-style reference entries // and correspondingly ordered items for given keylist. // APA uses alpha sort order. func RefsAPA(kl *KeyList) ([]rich.Text, []*Item) { refs := make([]rich.Text, kl.Len()) items := make([]*Item, kl.Len()) ks := kl.AlphaKeys() for i, k := range ks { it := kl.At(k) refs[i] = RefAPA(it) items[i] = it } return refs, items } func RefLinks(it *Item, tx *rich.Text) { link := rich.NewStyle().SetLinkStyle() if it.URL != "" { tx.AddLink(link, it.URL, it.URL) } if it.DOI != "" { url := " http://doi.org/" + it.DOI tx.AddLink(link, url, url) } } // EnsurePeriod returns a string that ends with a . if it doesn't // already end in some form of punctuation. func EnsurePeriod(s string) string { if !unicode.IsPunct(rune(s[len(s)-1])) { s += "." } return s } func RefAPABook(it *Item) rich.Text { sty := rich.NewStyle() ital := sty.Clone().SetSlant(rich.Italic) auths := "" if len(it.Author) > 0 { auths = NamesLastFirstInitialCommaAmpersand(it.Author) } else if len(it.Editor) > 0 { auths = NamesLastFirstInitialCommaAmpersand(it.Editor) + " (Ed" if len(it.Editor) == 1 { auths += ".)" } else { auths += "s.)" } } tx := rich.NewText(sty, []rune(auths+" ")) tx.AddSpanString(sty, "("+it.Issued.Year()+"). ") if it.Title != "" { ttl := it.Title end := rune(ttl[len(ttl)-1]) if it.Edition != "" { if unicode.IsPunct(end) { ttl = ttl[:len(ttl)-1] } else { end = '.' } tx.AddSpanString(ital, ttl) tx.AddSpanString(sty, " ("+it.Edition+" ed)"+string(end)+" ") } else { tx.AddSpanString(ital, EnsurePeriod(ttl)+" ") } } if it.Publisher != "" { tx.AddSpanString(sty, EnsurePeriod(it.Publisher)+" ") } RefLinks(it, &tx) return tx } func RefAPAChapter(it *Item) rich.Text { sty := rich.NewStyle() ital := sty.Clone().SetSlant(rich.Italic) tx := rich.NewText(sty, []rune(NamesLastFirstInitialCommaAmpersand(it.Author)+" ")) tx.AddSpanString(sty, "("+it.Issued.Year()+"). ") contStyle := ital if it.Title != "" { if len(it.Editor) == 0 || it.ContainerTitle == "" { contStyle = sty tx.AddSpanString(ital, EnsurePeriod(it.Title)+" ") } else { tx.AddSpanString(sty, EnsurePeriod(it.Title)+" ") } } if len(it.Editor) > 0 { eds := "In " + NamesFirstInitialLastCommaAmpersand(it.Editor) if len(it.Editor) == 1 { eds += " (Ed.), " } else { eds += " (Eds.), " } tx.AddSpanString(sty, eds) } else { tx.AddSpanString(sty, "In ") } if it.ContainerTitle != "" { ttl := it.ContainerTitle end := rune(ttl[len(ttl)-1]) pp := "" if it.Edition != "" { pp = "(" + it.Edition + " ed." } if it.Page != "" { if pp != "" { pp += ", " } else { pp = "(" } pp += "pp. " + it.Page } if pp != "" { pp = " " + pp + ")" if unicode.IsPunct(end) { ttl = ttl[:len(ttl)-1] pp += string(end) } else { pp += "." } tx.AddSpanString(contStyle, ttl) tx.AddSpanString(sty, pp+" ") } else { tx.AddSpanString(contStyle, EnsurePeriod(it.ContainerTitle)+" ") } } if it.Publisher != "" { tx.AddSpanString(sty, EnsurePeriod(it.Publisher)+" ") } RefLinks(it, &tx) return tx } func RefAPAArticle(it *Item) rich.Text { sty := rich.NewStyle() ital := sty.Clone().SetSlant(rich.Italic) tx := rich.NewText(sty, []rune(NamesLastFirstInitialCommaAmpersand(it.Author)+" ")) tx.AddSpanString(sty, "("+it.Issued.Year()+"). ") if it.Title != "" { tx.AddSpanString(sty, EnsurePeriod(it.Title)+" ") } jt := "" if it.ContainerTitle != "" { jt = it.ContainerTitle + ", " } if it.Volume != "" { jt += it.Volume } if jt != "" { tx.AddSpanString(ital, jt) } if it.Volume != "" { if it.Number != "" { tx.AddSpanString(sty, "("+it.Number+"), ") } else { tx.AddSpanString(sty, ", ") } } if it.Page != "" { tx.AddSpanString(sty, it.Page+". ") } RefLinks(it, &tx) return tx } func RefAPAThesis(it *Item) rich.Text { sty := rich.NewStyle() ital := sty.Clone().SetSlant(rich.Italic) tx := rich.NewText(sty, []rune(NamesLastFirstInitialCommaAmpersand(it.Author)+" ")) tx.AddSpanString(sty, "("+it.Issued.Year()+"). ") if it.Title != "" { tx.AddSpanString(ital, EnsurePeriod(it.Title)+" ") } tt := "[" if it.Source == "" { tt += "unpublished " } if it.Genre == "" { tt += "thesis" } else { tt += it.Genre } if it.Publisher != "" { tt += ", " + it.Publisher } tt += "]. " tx.AddSpanString(sty, tt) if it.Source != "" { tx.AddSpanString(sty, EnsurePeriod(it.Source)+" ") } RefLinks(it, &tx) return tx } func RefAPAMisc(it *Item) rich.Text { sty := rich.NewStyle() ital := sty.Clone().SetSlant(rich.Italic) tx := rich.NewText(sty, []rune(NamesLastFirstInitialCommaAmpersand(it.Author)+" ")) tx.AddSpanString(sty, "("+it.Issued.Year()+"). ") if it.Title != "" { tx.AddSpanString(ital, EnsurePeriod(it.Title)+" ") } jt := "" if it.ContainerTitle != "" { jt = it.ContainerTitle + ", " } if it.Volume != "" { jt += it.Volume } if jt != "" { tx.AddSpanString(sty, jt) } if it.Volume != "" { if it.Number != "" { tx.AddSpanString(sty, "("+it.Number+"), ") } else { tx.AddSpanString(sty, ", ") } } if it.Page != "" { tx.AddSpanString(sty, it.Page+". ") } if it.Genre != "" { tx.AddSpanString(sty, EnsurePeriod(it.Genre)+" ") } else { tx.AddSpanString(sty, it.Type.String()+". ") } if it.Source != "" { tx.AddSpanString(sty, EnsurePeriod(it.Source)+" ") } if it.Publisher != "" { tx.AddSpanString(sty, EnsurePeriod(it.Publisher)+" ") } RefLinks(it, &tx) return tx } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package csl import "strings" // The CSL input model supports two different date representations: // an EDTF string (preferred), and a more structured alternative. type Date struct { DateParts [][]any `json:"date-parts,omitempty"` Season any `json:"season,omitempty"` Circa string `json:"circa,omitempty"` Literal string `json:"literal,omitempty"` Raw string `json:"raw,omitempty"` } func (dt *Date) Year() string { if len(dt.DateParts) > 0 { if len(dt.DateParts[0]) > 0 { return dt.DateParts[0][0].(string) // this is normally it } } str := dt.Literal if str == "" { str = dt.Raw } if str == "" { str = dt.Circa } if str == "" { return "undated" } fs := strings.Fields(str) for _, s := range fs { if len(s) == 4 { return s } } return str } // Code generated by "core generate"; DO NOT EDIT. package csl import ( "cogentcore.org/core/enums" ) var _StylesValues = []Styles{0} // StylesN is the highest valid value for type Styles, plus one. const StylesN Styles = 1 var _StylesValueMap = map[string]Styles{`APA`: 0} var _StylesDescMap = map[Styles]string{0: ``} var _StylesMap = map[Styles]string{0: `APA`} // String returns the string representation of this Styles value. func (i Styles) String() string { return enums.String(i, _StylesMap) } // SetString sets the Styles value from its string representation, // and returns an error if the string is invalid. func (i *Styles) SetString(s string) error { return enums.SetString(i, s, _StylesValueMap, "Styles") } // Int64 returns the Styles value as an int64. func (i Styles) Int64() int64 { return int64(i) } // SetInt64 sets the Styles value from an int64. func (i *Styles) SetInt64(in int64) { *i = Styles(in) } // Desc returns the description of the Styles value. func (i Styles) Desc() string { return enums.Desc(i, _StylesDescMap) } // StylesValues returns all possible values for the type Styles. func StylesValues() []Styles { return _StylesValues } // Values returns all possible values for the type Styles. func (i Styles) Values() []enums.Enum { return enums.Values(_StylesValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Styles) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Styles) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Styles") } var _CiteStylesValues = []CiteStyles{0, 1} // CiteStylesN is the highest valid value for type CiteStyles, plus one. const CiteStylesN CiteStyles = 2 var _CiteStylesValueMap = map[string]CiteStyles{`Parenthetical`: 0, `Narrative`: 1} var _CiteStylesDescMap = map[CiteStyles]string{0: `Parenthetical means that the citation is placed within parentheses. This is default for most styles. In the APA style for example, it adds a comma before the year, e.g., "(Smith, 1989)". Note that the parentheses or other outer bracket syntax are NOT generated directly, because often multiple are included together in the same group.`, 1: `Narrative is an active, "inline" form of citation where the cited content is used as the subject of a sentence. In the APA style this puts the year in parentheses, e.g., "Smith (1989) invented the..." In this case the parentheses are generated.`} var _CiteStylesMap = map[CiteStyles]string{0: `Parenthetical`, 1: `Narrative`} // String returns the string representation of this CiteStyles value. func (i CiteStyles) String() string { return enums.String(i, _CiteStylesMap) } // SetString sets the CiteStyles value from its string representation, // and returns an error if the string is invalid. func (i *CiteStyles) SetString(s string) error { return enums.SetString(i, s, _CiteStylesValueMap, "CiteStyles") } // Int64 returns the CiteStyles value as an int64. func (i CiteStyles) Int64() int64 { return int64(i) } // SetInt64 sets the CiteStyles value from an int64. func (i *CiteStyles) SetInt64(in int64) { *i = CiteStyles(in) } // Desc returns the description of the CiteStyles value. func (i CiteStyles) Desc() string { return enums.Desc(i, _CiteStylesDescMap) } // CiteStylesValues returns all possible values for the type CiteStyles. func CiteStylesValues() []CiteStyles { return _CiteStylesValues } // Values returns all possible values for the type CiteStyles. func (i CiteStyles) Values() []enums.Enum { return enums.Values(_CiteStylesValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i CiteStyles) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *CiteStyles) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "CiteStyles") } 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} // TypesN is the highest valid value for type Types, plus one. const TypesN Types = 45 var _TypesValueMap = map[string]Types{`article`: 0, `article-journal`: 1, `article-magazine`: 2, `article-newspaper`: 3, `bill`: 4, `book`: 5, `broadcast`: 6, `chapter`: 7, `classic`: 8, `collection`: 9, `dataset`: 10, `document`: 11, `entry`: 12, `entry-dictionary`: 13, `entry-encyclopedia`: 14, `event`: 15, `figure`: 16, `graphic`: 17, `hearing`: 18, `interview`: 19, `legal-case`: 20, `legislation`: 21, `manuscript`: 22, `map`: 23, `motion-picture`: 24, `musical-score`: 25, `pamphlet`: 26, `paper-conference`: 27, `patent`: 28, `performance`: 29, `periodical`: 30, `personal-communication`: 31, `post`: 32, `post-weblog`: 33, `regulation`: 34, `report`: 35, `review`: 36, `review-book`: 37, `software`: 38, `song`: 39, `speech`: 40, `standard`: 41, `thesis`: 42, `treaty`: 43, `webpage`: 44} var _TypesDescMap = map[Types]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: ``, 27: ``, 28: ``, 29: ``, 30: ``, 31: ``, 32: ``, 33: ``, 34: ``, 35: ``, 36: ``, 37: ``, 38: ``, 39: ``, 40: ``, 41: ``, 42: ``, 43: ``, 44: ``} var _TypesMap = map[Types]string{0: `article`, 1: `article-journal`, 2: `article-magazine`, 3: `article-newspaper`, 4: `bill`, 5: `book`, 6: `broadcast`, 7: `chapter`, 8: `classic`, 9: `collection`, 10: `dataset`, 11: `document`, 12: `entry`, 13: `entry-dictionary`, 14: `entry-encyclopedia`, 15: `event`, 16: `figure`, 17: `graphic`, 18: `hearing`, 19: `interview`, 20: `legal-case`, 21: `legislation`, 22: `manuscript`, 23: `map`, 24: `motion-picture`, 25: `musical-score`, 26: `pamphlet`, 27: `paper-conference`, 28: `patent`, 29: `performance`, 30: `periodical`, 31: `personal-communication`, 32: `post`, 33: `post-weblog`, 34: `regulation`, 35: `report`, 36: `review`, 37: `review-book`, 38: `software`, 39: `song`, 40: `speech`, 41: `standard`, 42: `thesis`, 43: `treaty`, 44: `webpage`} // 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") } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package csl import ( "io/fs" "os" "time" "cogentcore.org/core/base/iox/jsonx" "cogentcore.org/core/text/parse/languages/bibtex" ) // Open opens CSL data items from a .json formatted CSL file. func Open(filename string) ([]Item, error) { var its []Item err := jsonx.Open(&its, filename) return its, err } // OpenFS opens CSL data items from a .json formatted CSL file from given // filesystem. func OpenFS(fsys fs.FS, filename string) ([]Item, error) { var its []Item err := jsonx.OpenFS(&its, fsys, filename) return its, err } // SaveItems saves items to given filename. func SaveItems(items []Item, filename string) error { return jsonx.Save(items, filename) } // SaveKeyList saves items to given filename. func SaveKeyList(kl *KeyList, filename string) error { return jsonx.Save(kl.Values, filename) } //////// File // File maintains a record for a CSL file. type File struct { // File name, full path. File string // Items from the file, as a KeyList for easy citation lookup. Items *KeyList // mod time for loaded file, to detect updates. Mod time.Time } // 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 is already loaded, then nothing happens (already have it), but // otherwise it parses the file and puts contents in Items. func (fl *File) Open(fname string) error { path := fname var err error if fl.File == "" { path, err = bibtex.FullPath(fname) if err != nil { return err } fl.File = path fl.Items = 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.Items != 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 } its, err := Open(fl.File) if err != nil { return err } fl.Items = NewKeyList(its) fl.Mod = st.ModTime() // fmt.Printf("(re)loaded bibtex bibliography: %s\n", fl.File) return nil } //////// Files // Files is a map of CSL items keyed by file name. 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 Items is already loaded, then nothing happens (already have it), but // otherwise it parses the file and puts contents in Items 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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package csl import ( "fmt" "slices" "strings" "cogentcore.org/core/base/keylist" ) // KeyList is an ordered list of citation [Item]s, // which should be used to collect items by unique citation keys. type KeyList struct { keylist.List[string, *Item] } // NewKeyList returns a KeyList from given list of [Item]s. func NewKeyList(items []Item) *KeyList { kl := &KeyList{} for i := range items { it := &items[i] kl.Add(it.CitationKey, it) } return kl } // AlphaKeys returns an alphabetically sorted list of keys. func (kl *KeyList) AlphaKeys() []string { ks := slices.Clone(kl.Keys) slices.Sort(ks) return ks } // PrettyString pretty prints the items using default style. func (kl *KeyList) PrettyString() string { var w strings.Builder for _, it := range kl.Values { w.WriteString(fmt.Sprintf("%s [%s]:\n", it.CitationKey, it.Type)) w.WriteString(string(Ref(DefaultStyle, it).Join()) + "\n\n") } return w.String() } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package csl import ( "bufio" "io" "os" "path/filepath" "regexp" "strings" "cogentcore.org/core/base/errors" "cogentcore.org/core/base/fsx" ) // GenerateMarkdown extracts markdown citations in the format [@Ref; @Ref] // from .md markdown files in given directory, looking up in given source [KeyList], // and writing the results in given style to given .md file (references.md default). // Heading is written first: must include the appropriate markdown heading level // (## typically). Returns the [KeyList] of references that were cited. func GenerateMarkdown(dir, refFile, heading string, kl *KeyList, sty Styles) (*KeyList, error) { cited := &KeyList{} if dir == "" { dir = "./" } mds := fsx.Filenames(dir, ".md") if len(mds) == 0 { return cited, errors.New("No .md files found in: " + dir) } var errs []error for i := range mds { mds[i] = filepath.Join(dir, mds[i]) } err := ExtractMarkdownCites(mds, kl, cited) if err != nil { errs = append(errs, err) } if refFile == "" { refFile = filepath.Join(dir, "references.md") } of, err := os.Create(refFile) if err != nil { errs = append(errs, err) return cited, errors.Join(errs...) } defer of.Close() if heading != "" { of.WriteString(heading + "\n\n") } err = WriteRefsMarkdown(of, cited, sty) if err != nil { errs = append(errs, err) } return cited, errors.Join(errs...) } // ExtractMarkdownCites extracts markdown citations in the format [@Ref; @Ref] // from given list of .md files, looking up in given source [KeyList], adding to cited. func ExtractMarkdownCites(files []string, src, cited *KeyList) error { exp := regexp.MustCompile(`\[(@\^?([[:alnum:]]+-?)+(;[[:blank:]]+)?)+\]`) var errs []error for _, fn := range files { f, err := os.Open(fn) if err != nil { errs = append(errs, err) continue } scan := bufio.NewScanner(f) for scan.Scan() { cs := exp.FindAllString(string(scan.Bytes()), -1) for _, c := range cs { tc := c[1 : len(c)-1] sp := strings.Split(tc, "@") for _, ac := range sp { a := strings.TrimSpace(ac) a = strings.TrimSuffix(a, ";") if a == "" { continue } if a[0] == '^' { a = a[1:] } it, has := src.AtTry(a) if !has { err = errors.New("citation not found: " + a) errs = append(errs, err) continue } cited.Add(a, it) } } } f.Close() } return errors.Join(errs...) } // WriteRefsMarkdown writes references from given [KeyList] to a // markdown file. func WriteRefsMarkdown(w io.Writer, kl *KeyList, sty Styles) error { refs, items := Refs(sty, kl) for i, ref := range refs { it := items[i] _, err := w.Write([]byte(`<p id="` + it.CitationKey + `">`)) if err != nil { return err } _, err = w.Write([]byte(string(ref.Join()) + "</p>\n\n")) // todo: ref to markdown!! if err != nil { return err } } return nil } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "cogentcore.org/core/cli" "cogentcore.org/core/text/csl" ) //go:generate core generate -add-types -add-funcs type Config struct { // CSL JSON formatted file with the library of references to lookup citations in. Refs string `flag:"r,refs" posarg:"0"` // Directory with markdown files to extract citations from. // Defaults to current directory if empty. Dir string `flag:"d,dir"` // File name to write the formatted references to. // Defaults to references.md if empty. Output string `flag:"o,output"` // File name to write the subset of cited reference data to. // Defaults to citedrefs.json if empty. CitedData string `flag:"c,cited"` // heading to add to the top of the references file. // Include markdown heading syntax, e.g., ## // Defaults to ## References if empty. Heading string `flag:"h,heading"` // style is the citation style to generate. // Defaults to APA if empty. Style csl.Styles `flag:"s,style"` } // Generate extracts citations and generates resulting references file. func Generate(c *Config) error { refs, err := csl.Open(c.Refs) if err != nil { return err } kl := csl.NewKeyList(refs) cited, err := csl.GenerateMarkdown(c.Dir, c.Output, c.Heading, kl, c.Style) cf := c.CitedData if cf == "" { cf = "citedrefs.json" } csl.SaveKeyList(cited, cf) return err } func main() { //types:skip opts := cli.DefaultOptions("mdcite", "mdcites extracts markdown citations from .md files in a directory, and writes a references file with the resulting citations, using the default APA style or the specified one.") cli.Run(opts, &Config{}, Generate) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package csl import ( "fmt" "strings" ) // Name represents a persons name. type Name struct { Family string `json:"family,omitempty"` Given string `json:"given,omitempty"` DroppingParticle string `json:"dropping-particle,omitempty"` NonDroppingParticle string `json:"non-dropping-particle,omitempty"` Suffix string `json:"suffix,omitempty"` CommaSuffix any `json:"comma-suffix,omitempty"` StaticOrdering any `json:"static-ordering,omitempty"` Literal string `json:"literal,omitempty"` ParseNames any `json:"parse-names,omitempty"` } // NameFamilyGiven returns the family and given names from given name record, // parsing what is available if not already parsed. // todo: add suffix stuff! func NameFamilyGiven(nm *Name) (family, given string) { if nm.Family != "" && nm.Given != "" { return nm.Family, nm.Given } pnm := "" switch { case nm.Family != "": pnm = nm.Family case nm.Given != "": pnm = nm.Given case nm.Literal != "": pnm = nm.Literal } if pnm == "" { fmt.Printf("csl.NameFamilyGiven name format error: no valid name: %#v\n", nm) return } ci := strings.Index(pnm, ",") if ci > 0 { return pnm[:ci], strings.TrimSpace(pnm[ci+1:]) } fs := strings.Fields(pnm) nfs := len(fs) if nfs > 1 { return fs[nfs-1], strings.Join(fs[:nfs-1], " ") } return pnm, "" } // NamesLastFirstInitialCommaAmpersand returns a list of names // formatted as a string, in the format: Last, F., Last, F., & Last., F. func NamesLastFirstInitialCommaAmpersand(nms []Name) string { var w strings.Builder n := len(nms) for i := range nms { nm := &nms[i] fam, giv := NameFamilyGiven(nm) w.WriteString(fam) if giv != "" { w.WriteString(", ") nf := strings.Fields(giv) for _, fn := range nf { w.WriteString(fn[0:1] + ".") } } if i == n-1 { break } if i == n-2 { w.WriteString(", & ") } else { w.WriteString(", ") } } return w.String() } // NamesFirstInitialLastCommaAmpersand returns a list of names // formatted as a string, in the format: A.B. Last, C.D., Last & L.M. Last func NamesFirstInitialLastCommaAmpersand(nms []Name) string { var w strings.Builder n := len(nms) for i := range nms { nm := &nms[i] fam, giv := NameFamilyGiven(nm) if giv != "" { nf := strings.Fields(giv) for _, fn := range nf { w.WriteString(fn[0:1] + ".") } w.WriteString(" ") } w.WriteString(fam) if i == n-1 { break } if i == n-2 { w.WriteString(", & ") } else { w.WriteString(", ") } } return w.String() } // NamesCiteEtAl returns a list of names formatted for a // citation within a document, as Last [et al..] or // Last & Last if exactly two authors. func NamesCiteEtAl(nms []Name) string { var w strings.Builder n := len(nms) switch { case n == 0: return "(None)" case n == 1: fam, _ := NameFamilyGiven(&nms[0]) w.WriteString(fam) case n == 2: fam, _ := NameFamilyGiven(&nms[0]) w.WriteString(fam) w.WriteString(" & ") fam, _ = NameFamilyGiven(&nms[1]) w.WriteString(fam) default: fam, _ := NameFamilyGiven(&nms[0]) w.WriteString(fam) w.WriteString(" et al.") } return w.String() } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package csl import "cogentcore.org/core/text/rich" // DefaultStyle is the default citation and reference formatting style. var DefaultStyle = APA // Styles are CSL citation and reference formatting styles. type Styles int32 //enums:enum const ( APA Styles = iota ) // CiteStyles are different types of citation styles that are supported by // some formatting [Styles]. type CiteStyles int32 //enums:enum const ( // Parenthetical means that the citation is placed within parentheses. // This is default for most styles. In the APA style for example, it // adds a comma before the year, e.g., "(Smith, 1989)". // Note that the parentheses or other outer bracket syntax are NOT // generated directly, because often multiple are included together // in the same group. Parenthetical CiteStyles = iota // Narrative is an active, "inline" form of citation where the cited // content is used as the subject of a sentence. In the APA style this // puts the year in parentheses, e.g., "Smith (1989) invented the..." // In this case the parentheses are generated. Narrative ) // Ref generates the reference text for given item, // according to the given style. func Ref(s Styles, it *Item) rich.Text { switch s { case APA: return RefAPA(it) } return nil } // Refs returns a list of references and matching items // according to the given [Styles] style. func Refs(s Styles, kl *KeyList) ([]rich.Text, []*Item) { switch s { case APA: return RefsAPA(kl) } return nil, nil } // RefsDefault returns a list of references and matching items // according to the [DefaultStyle]. func RefsDefault(kl *KeyList) ([]rich.Text, []*Item) { return Refs(DefaultStyle, kl) } // Cite generates the citation text for given item, // according to the given overall style an citation style. func Cite(s Styles, cs CiteStyles, it *Item) string { switch s { case APA: return CiteAPA(cs, it) } return "" } // CiteDefault generates the citation text for given item, // according to the [DefaultStyle] overall style, and given [CiteStyles]. func CiteDefault(cs CiteStyles, it *Item) string { return Cite(DefaultStyle, cs, it) } // 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/text/textcore" "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 textcore.DiffEditor func (br *Browser) ViewDiff(fn *Node) *textcore.DiffEditor { df := fsx.DirAndFile(fn.FileA) tabs := br.Tabs() tab := tabs.RecycleTab(df) if tab.HasChildren() { dv := tab.Child(1).(*textcore.DiffEditor) return dv } tb := core.NewToolbar(tab) de := textcore.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/text/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/text/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 } 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 } } } } // 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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main //go:generate core generate -add-types import ( "bytes" "fmt" "math" "os" "strconv" "strings" "cogentcore.org/core/base/errors" "cogentcore.org/core/base/keylist" "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/icons" "cogentcore.org/core/keymap" "cogentcore.org/core/styles" "cogentcore.org/core/styles/states" "cogentcore.org/core/text/fonts" "cogentcore.org/core/text/rich" "cogentcore.org/core/tree" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/font/opentype" ) // Browser is a font browser. type Browser struct { core.Frame Filename core.Filename IsEmoji bool // true if an emoji font Font *font.Face RuneMap *keylist.List[rune, font.GID] } // OpenFile opens a font file. func (fb *Browser) OpenFile(fname core.Filename) error { //types:add return fb.OpenFileIndex(fname, 0) } // OpenFileIndex opens a font file. func (fb *Browser) OpenFileIndex(fname core.Filename, index int) error { //types:add b, err := os.ReadFile(string(fname)) if errors.Log(err) != nil { return err } fb.Filename = fname fb.IsEmoji = strings.Contains(strings.ToLower(string(fb.Filename)), "emoji") if fb.IsEmoji { core.MessageSnackbar(fb, "Opening emoji font: "+string(fb.Filename)+" will take a while to render..") } return fb.OpenFontData(b, index) } // SelectFont selects a font from among a loaded list. func (fb *Browser) SelectFont() { //types:add d := core.NewBody("Select Font") d.SetTitle("Select a font family") si := 0 fl := fb.Scene.TextShaper().FontList() fi := fonts.Families(fl) tb := core.NewTable(d) tb.SetSlice(&fi).SetSelectedField("Family"). SetSelectedValue(fb.Font.Describe().Family).BindSelect(&si) tb.SetTableStyler(func(w core.Widget, s *styles.Style, row, col int) { if col != 1 { return } s.Font.CustomFont = rich.FontName(fi[row].Family) s.Font.Family = rich.Custom s.Font.Size.Dp(24) }) d.AddBottomBar(func(bar *core.Frame) { d.AddOK(bar).OnClick(func(e events.Event) { fam := fi[si].Family idx := 0 for i := range fl { if fl[i].Family == fam && (fl[i].Weight == rich.Medium || fl[i].Weight == rich.Normal) { idx = i break } } loc := fl[idx].Font.Location finfo := fmt.Sprintf("loading font: %s from: %s idx: %d, sel: %d", fam, loc.File, loc.Index, si) fmt.Println(finfo) core.MessageSnackbar(fb, finfo) fb.OpenFileIndex(core.Filename(loc.File), int(loc.Index)) }) }) d.RunWindowDialog(fb) } // OpenFontData opens given font data. func (fb *Browser) OpenFontData(b []byte, index int) error { faces, err := font.ParseTTC(bytes.NewReader(b)) if errors.Log(err) != nil { return err } // fmt.Println("number of faces:", len(faces), "index:", index) fb.Font = faces[index] for i, fnt := range faces { d := fnt.Describe() fmt.Println("index:", i, "family:", d.Family, "Aspect:", d.Aspect) } fb.UpdateRuneMap() fb.Update() return nil } func (fb *Browser) UpdateRuneMap() { fb.DeleteChildren() fb.RuneMap = keylist.New[rune, font.GID]() if fb.Font == nil { return } // for _, pr := range unicode.PrintRanges { // for _, rv := range pr.R16 { // for r := rv.Lo; r <= rv.Hi; r += rv.Stride { // gid, has := fb.Font.NominalGlyph(rune(r)) // if !has { // continue // } // fb.RuneMap.Add(rune(r), gid) // } // } // } if fb.IsEmoji { // for r := rune(0); r < math.MaxInt16; r++ { for r := rune(0); r < math.MaxInt32; r++ { // takes a LONG time.. gid, has := fb.Font.NominalGlyph(r) if !has { continue } fb.RuneMap.Add(r, gid) } } else { for r := rune(0); r < math.MaxInt16; r++ { gid, has := fb.Font.NominalGlyph(r) if !has { continue } fb.RuneMap.Add(r, gid) } } } // SelectRune selects a rune in current font (first char) of string. func (fb *Browser) SelectRune(r string) { //types:add rs := []rune(r) if len(rs) == 0 { core.MessageSnackbar(fb, "no runes!") return } ix := fb.RuneMap.IndexByKey(rs[0]) if ix < 0 { core.MessageSnackbar(fb, "rune not found!") return } gi := fb.Child(ix).(core.Widget).AsWidget() gi.Styles.State.SetFlag(true, states.Selected, states.Active) gi.SetFocus() core.MessageSnackbar(fb, fmt.Sprintf("rune %s at index: %d GID: %d", r, ix, fb.RuneMap.Values[ix])) } // SelectRuneInt selects a rune in current font by number func (fb *Browser) SelectRuneInt(r int) { //types:add ix := fb.RuneMap.IndexByKey(rune(r)) if ix < 0 { core.MessageSnackbar(fb, "rune not found!") return } gi := fb.Child(ix).(core.Widget).AsWidget() gi.Styles.State.SetFlag(true, states.Selected, states.Active) gi.SetFocus() core.MessageSnackbar(fb, fmt.Sprintf("rune %s at index: %d GID: %d", string(rune(r)), ix, fb.RuneMap.Values[ix])) } // SelectGlyphID selects glyphID in current font. func (fb *Browser) SelectGlyphID(gid opentype.GID) { //types:add ix := -1 for i, g := range fb.RuneMap.Values { if gid == g { ix = i break } } if ix < 0 { core.MessageSnackbar(fb, "glyph id not found!") return } r := string(rune(fb.RuneMap.Keys[ix])) gi := fb.Child(ix).(core.Widget).AsWidget() gi.Styles.State.SetFlag(true, states.Selected, states.Active) gi.SetFocus() core.MessageSnackbar(fb, fmt.Sprintf("rune %s at index: %d GID: %d", r, ix, fb.RuneMap.Values[ix])) } // SaveUnicodes saves all the unicodes in hex format to a file called unicodes.md func (fb *Browser) SaveUnicodes() { //types:add var b strings.Builder for _, r := range fb.RuneMap.Keys { b.WriteString(fmt.Sprintf("%X\n", r)) } os.WriteFile("unicodes.md", []byte(b.String()), 0666) } func (fb *Browser) Init() { fb.Frame.Init() fb.Styler(func(s *styles.Style) { // s.Display = styles.Flex // s.Wrap = true // s.Direction = styles.Row s.Display = styles.Grid s.Columns = 32 }) fb.Maker(func(p *tree.Plan) { if fb.Font == nil { return } for i, gid := range fb.RuneMap.Values { r := fb.RuneMap.Keys[i] nm := string(r) + "_" + strconv.Itoa(int(r)) tree.AddAt(p, nm, func(w *Glyph) { w.SetBrowser(fb).SetRune(r).SetGID(gid) }) } }) } func (fb *Browser) MakeToolbar(p *tree.Plan) { tree.Add(p, func(w *core.FuncButton) { w.SetFunc(fb.OpenFile).SetIcon(icons.Open).SetKey(keymap.Open) w.Args[0].SetValue(fb.Filename).SetTag(`extension:".ttf"`) }) tree.Add(p, func(w *core.FuncButton) { w.SetFunc(fb.SelectFont).SetIcon(icons.Open) }) tree.Add(p, func(w *core.FuncButton) { w.SetFunc(fb.SelectEmbedded).SetIcon(icons.Open) }) tree.Add(p, func(w *core.FuncButton) { w.SetFunc(fb.SelectRune).SetIcon(icons.Select) }) tree.Add(p, func(w *core.FuncButton) { w.SetFunc(fb.SelectRuneInt).SetIcon(icons.Select) }) tree.Add(p, func(w *core.FuncButton) { w.SetFunc(fb.SelectGlyphID).SetIcon(icons.Select) }) tree.Add(p, func(w *core.FuncButton) { w.SetFunc(fb.SaveUnicodes).SetIcon(icons.Save) }) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "cogentcore.org/core/base/slicesx" "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/paint/ppath" "cogentcore.org/core/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/styles/states" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/fonts" "cogentcore.org/core/text/rich" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/font/opentype" ) // GlyphInfo returns info about a glyph. type GlyphInfo struct { // Rune is the unicode rune as a string Rune string // RuneInt is the unicode code point, int number. RuneInt rune // RuneHex is the unicode code point, hexidecimal number. RuneHex rune `format:"%0X"` // GID is the glyph ID, specific to each Font. GID font.GID // HAdvance is the horizontal advance. HAdvance float32 // Extents give the size of the glyph. Extents opentype.GlyphExtents // Extents are the horizontal font size parameters. HExtents font.FontExtents // Outline has the end points of each segment of the outline. Outline []math32.Vector2 } func NewGlyphInfo(face *font.Face, r rune, gid font.GID) *GlyphInfo { gi := &GlyphInfo{} gi.Set(face, r, gid) return gi } // Set sets the info from given [font.Face] and gid. func (gi *GlyphInfo) Set(face *font.Face, r rune, gid font.GID) { gi.Rune = string(r) gi.RuneInt = r gi.RuneHex = r gi.GID = gid gi.HAdvance = face.HorizontalAdvance(gid) gi.HExtents, _ = face.FontHExtents() gi.Extents, _ = face.GlyphExtents(gid) } // Glyph displays an individual glyph in the browser type Glyph struct { core.Canvas // Rune is the rune to render. Rune rune // GID is the glyph ID of the Rune GID font.GID // Outline is the set of control points (end points only). Outline []math32.Vector2 `set:"-"` // Stroke only renders the outline of the glyph, not the standard fill. Stroke bool // Points plots the control points. Points bool Browser *Browser } func (gi *Glyph) Init() { gi.Canvas.Init() gi.Styler(func(s *styles.Style) { s.Min.Set(units.Em(3)) s.SetTextWrap(false) s.Cursor = cursors.Pointer if gi.Browser == nil { return } s.SetAbilities(true, abilities.Clickable, abilities.Focusable, abilities.Activatable, abilities.Selectable) sty, tsty := s.NewRichText() fonts.Style(gi.Browser.Font, sty, tsty) }) gi.OnClick(func(e events.Event) { if gi.Stroke || gi.Browser == nil || gi.Browser.Font == nil { return } gli := NewGlyphInfo(gi.Browser.Font, gi.Rune, gi.GID) gli.Outline = gi.Outline d := core.NewBody("Glyph Info") bg := NewGlyph(d).SetBrowser(gi.Browser).SetRune(gi.Rune).SetGID(gi.GID). SetStroke(true).SetPoints(true) bg.Styler(func(s *styles.Style) { s.Min.Set(units.Em(40)) }) core.NewForm(d).SetStruct(gli).StartFocus() d.AddBottomBar(func(bar *core.Frame) { d.AddOK(bar) }) d.RunWindowDialog(gi.Browser) }) gi.SetDraw(gi.draw) } func (gi *Glyph) drawShaped(pc *paint.Painter) { sty, tsty := gi.Styles.NewRichText() fonts.Style(gi.Browser.Font, sty, tsty) sz := gi.Geom.Size.Actual.Content msz := min(sz.X, sz.Y) sty.Size = float32(msz) / tsty.FontSize.Dots sty.Size *= 0.85 tx := rich.NewText(sty, []rune{gi.Rune}) lns := gi.Scene.TextShaper().WrapLines(tx, sty, tsty, &core.AppearanceSettings.Text, sz) off := math32.Vec2(0, 0) if msz > 200 { o := 0.2 * float32(msz) if gi.Browser.IsEmoji { off = math32.Vec2(0.5*o, -o) } else { // for bitmap fonts, kinda random off = math32.Vec2(o, o) } } pc.DrawText(lns, gi.Geom.Pos.Content.Add(off)) } func (gi *Glyph) draw(pc *paint.Painter) { if gi.Browser == nil || gi.Browser.Font == nil { return } face := gi.Browser.Font data := face.GlyphData(gi.GID) gd, ok := data.(font.GlyphOutline) if !ok { gi.drawShaped(pc) return } scale := 0.7 / float32(face.Upem()) x := float32(0.1) y := float32(0.8) gi.Outline = slicesx.SetLength(gi.Outline, len(gd.Segments)) pc.Fill.Color = colors.Scheme.Surface if gi.StateIs(states.Active) || gi.StateIs(states.Focused) || gi.StateIs(states.Selected) { pc.Fill.Color = colors.Scheme.Select.Container } pc.Stroke.Color = colors.Scheme.OnSurface pc.Rectangle(0, 0, 1, 1) pc.Draw() pc.Fill.Color = nil pc.Line(0, y, 1, y) pc.Draw() if gi.Stroke { pc.Stroke.Width.Dp(2) pc.Stroke.Color = colors.Scheme.OnSurface pc.Fill.Color = nil } else { pc.Stroke.Color = nil pc.Fill.Color = colors.Scheme.OnSurface } ext, _ := face.GlyphExtents(gi.GID) if ext.XBearing < 0 { x -= scale * ext.XBearing } var gp ppath.Path for i, s := range gd.Segments { px := s.Args[0].X*scale + x py := -s.Args[0].Y*scale + y switch s.Op { case opentype.SegmentOpMoveTo: gp.MoveTo(px, py) gi.Outline[i] = math32.Vec2(px, py) case opentype.SegmentOpLineTo: gp.LineTo(px, py) gi.Outline[i] = math32.Vec2(px, py) case opentype.SegmentOpQuadTo: p1x := s.Args[1].X*scale + x p1y := -s.Args[1].Y*scale + y gp.QuadTo(px, py, p1x, p1y) gi.Outline[i] = math32.Vec2(p1x, p1y) case opentype.SegmentOpCubeTo: p1x := s.Args[1].X*scale + x p1y := -s.Args[1].Y*scale + y p2x := s.Args[2].X*scale + x p2y := -s.Args[2].Y*scale + y gp.CubeTo(px, py, p1x, p1y, p2x, p2y) gi.Outline[i] = math32.Vec2(p2x, p2y) } } bb := gp.FastBounds() sx := float32(1) sy := float32(1) if bb.Max.X >= 0.98 { sx = 0.9 / bb.Max.X } if bb.Min.Y < 0 { sy = 0.9 * (1 + bb.Min.Y) / 1.0 gp = gp.Translate(0, -bb.Min.Y/sy) y -= bb.Min.Y / sy } if bb.Max.Y > 1 { sy *= 0.9 / bb.Max.Y } if sx != 1 || sy != 1 { gp = gp.Scale(sx, sy) } pc.State.Path = gp pc.Draw() // Points if !gi.Points { return } pc.Stroke.Color = nil pc.Fill.Color = colors.Scheme.Primary.Base radius := float32(0.01) for _, s := range gd.Segments { px := sx * (s.Args[0].X*scale + x) py := sy * (-s.Args[0].Y*scale + y) switch s.Op { case opentype.SegmentOpMoveTo, opentype.SegmentOpLineTo: pc.Circle(px, py, radius) case opentype.SegmentOpQuadTo: p1x := sx * (s.Args[1].X*scale + x) p1y := sy * (-s.Args[1].Y*scale + y) pc.Circle(p1x, p1y, radius) case opentype.SegmentOpCubeTo: p2x := sx * (s.Args[2].X*scale + x) p2y := sy * (-s.Args[2].Y*scale + y) pc.Circle(p2x, p2y, radius) } } pc.Draw() radius *= 0.8 pc.Stroke.Color = nil pc.Fill.Color = colors.Scheme.Error.Base for _, s := range gd.Segments { px := sx * (s.Args[0].X*scale + x) py := sy * (-s.Args[0].Y*scale + y) switch s.Op { case opentype.SegmentOpQuadTo: pc.Circle(px, py, radius) case opentype.SegmentOpCubeTo: p1x := sx * (s.Args[1].X*scale + x) p1y := sy * (-s.Args[1].Y*scale + y) pc.Circle(px, py, radius) pc.Circle(p1x, p1y, radius) } } pc.Draw() pc.Stroke.Color = colors.Scheme.Error.Base pc.Fill.Color = nil for _, s := range gd.Segments { px := sx * (s.Args[0].X*scale + x) py := sy * (-s.Args[0].Y*scale + y) switch s.Op { case opentype.SegmentOpQuadTo: p1x := sx * (s.Args[1].X*scale + x) p1y := sy * (-s.Args[1].Y*scale + y) pc.Line(p1x, p1y, px, py) case opentype.SegmentOpCubeTo: p1x := sx * (s.Args[1].X*scale + x) p1y := sy * (-s.Args[1].Y*scale + y) p2x := sx * (s.Args[2].X*scale + x) p2y := sy * (-s.Args[2].Y*scale + y) pc.Line(px, py, p2x, p2y) pc.Line(p1x, p1y, p2x, p2y) } } pc.Draw() } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import "cogentcore.org/core/core" func main() { b := core.NewBody() fb := NewBrowser(b) fb.OpenFile("../noto/NotoSans-Regular.ttf") b.AddTopBar(func(bar *core.Frame) { core.NewToolbar(bar).Maker(fb.MakeToolbar) }) b.RunMainWindow() } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/text/tex" ) func init() { tex.LMFontsLoad() } // SelectEmbedded selects an embedded font from a list. func (fb *Browser) SelectEmbedded() { //types:add d := core.NewBody("Select Font") d.SetTitle("Select an embedded font") si := 0 fl := tex.LMFonts names := make([]string, len(fl)) for i := range fl { names[i] = fl[i].Family } tb := core.NewList(d) tb.SetSlice(&names).BindSelect(&si) d.AddBottomBar(func(bar *core.Frame) { d.AddOK(bar).OnClick(func(e events.Event) { fb.Font = fl[si].Fonts[0] fb.UpdateRuneMap() fb.Update() }) }) d.RunWindowDialog(fb) } // Code generated by "core generate -add-types"; DO NOT EDIT. package main import ( "cogentcore.org/core/base/keylist" "cogentcore.org/core/core" "cogentcore.org/core/tree" "cogentcore.org/core/types" "github.com/go-text/typesetting/font" ) var _ = types.AddType(&types.Type{Name: "main.Browser", IDName: "browser", Doc: "Browser is a font browser.", Methods: []types.Method{{Name: "OpenFile", Doc: "OpenFile opens a font file.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"fname"}, Returns: []string{"error"}}, {Name: "OpenFileIndex", Doc: "OpenFileIndex opens a font file.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"fname", "index"}, Returns: []string{"error"}}, {Name: "SelectFont", Doc: "SelectFont selects a font from among a loaded list.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "SelectRune", Doc: "SelectRune selects a rune in current font (first char) of string.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"r"}}, {Name: "SelectRuneInt", Doc: "SelectRuneInt selects a rune in current font by number", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"r"}}, {Name: "SelectGlyphID", Doc: "SelectGlyphID selects glyphID in current font.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"gid"}}, {Name: "SaveUnicodes", Doc: "SaveUnicodes saves all the unicodes in hex format to a file called unicodes.md", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "SelectEmbedded", Doc: "SelectEmbedded selects an embedded font from a list.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Filename"}, {Name: "IsEmoji"}, {Name: "Font"}, {Name: "RuneMap"}}}) // NewBrowser returns a new [Browser] with the given optional parent: // Browser is a font browser. func NewBrowser(parent ...tree.Node) *Browser { return tree.New[Browser](parent...) } // SetFilename sets the [Browser.Filename] func (t *Browser) SetFilename(v core.Filename) *Browser { t.Filename = v; return t } // SetIsEmoji sets the [Browser.IsEmoji] func (t *Browser) SetIsEmoji(v bool) *Browser { t.IsEmoji = v; return t } // SetFont sets the [Browser.Font] func (t *Browser) SetFont(v *font.Face) *Browser { t.Font = v; return t } // SetRuneMap sets the [Browser.RuneMap] func (t *Browser) SetRuneMap(v *keylist.List[rune, font.GID]) *Browser { t.RuneMap = v; return t } var _ = types.AddType(&types.Type{Name: "main.GlyphInfo", IDName: "glyph-info", Doc: "GlyphInfo returns info about a glyph.", Fields: []types.Field{{Name: "Rune", Doc: "Rune is the unicode rune as a string"}, {Name: "RuneInt", Doc: "RuneInt is the unicode code point, int number."}, {Name: "RuneHex", Doc: "RuneHex is the unicode code point, hexidecimal number."}, {Name: "GID", Doc: "GID is the glyph ID, specific to each Font."}, {Name: "HAdvance", Doc: "HAdvance is the horizontal advance."}, {Name: "Extents", Doc: "Extents give the size of the glyph."}, {Name: "HExtents", Doc: "Extents are the horizontal font size parameters."}, {Name: "Outline", Doc: "Outline has the end points of each segment of the outline."}}}) var _ = types.AddType(&types.Type{Name: "main.Glyph", IDName: "glyph", Doc: "Glyph displays an individual glyph in the browser", Embeds: []types.Field{{Name: "Canvas"}}, Fields: []types.Field{{Name: "Rune", Doc: "Rune is the rune to render."}, {Name: "GID", Doc: "GID is the glyph ID of the Rune"}, {Name: "Outline", Doc: "Outline is the set of control points (end points only)."}, {Name: "Stroke", Doc: "Stroke only renders the outline of the glyph, not the standard fill."}, {Name: "Points", Doc: "Points plots the control points."}, {Name: "Browser"}}}) // NewGlyph returns a new [Glyph] with the given optional parent: // Glyph displays an individual glyph in the browser func NewGlyph(parent ...tree.Node) *Glyph { return tree.New[Glyph](parent...) } // SetRune sets the [Glyph.Rune]: // Rune is the rune to render. func (t *Glyph) SetRune(v rune) *Glyph { t.Rune = v; return t } // SetGID sets the [Glyph.GID]: // GID is the glyph ID of the Rune func (t *Glyph) SetGID(v font.GID) *Glyph { t.GID = v; return t } // SetStroke sets the [Glyph.Stroke]: // Stroke only renders the outline of the glyph, not the standard fill. func (t *Glyph) SetStroke(v bool) *Glyph { t.Stroke = v; return t } // SetPoints sets the [Glyph.Points]: // Points plots the control points. func (t *Glyph) SetPoints(v bool) *Glyph { t.Points = v; return t } // SetBrowser sets the [Glyph.Browser] func (t *Glyph) SetBrowser(v *Browser) *Glyph { t.Browser = v; return t } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package fonts import ( "bytes" "cmp" "slices" "cogentcore.org/core/text/rich" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/fontscan" ) // Info contains basic font information for aviailable fonts. // This is used for a chooser for example. type Info struct { // Family name. Family string // Weight: normal, bold, etc Weight rich.Weights // Slant: normal or italic Slant rich.Slants // Stretch: normal, expanded, condensed, etc Stretch rich.Stretch // Font contains info about the location, family, etc of the font file. Font fontscan.Footprint `display:"-"` } // Family is used for selecting a font family in a font chooser. type Family struct { // Family name. Family string // example text, styled according to font family in chooser. Example string } // InfoExample is example text to demonstrate fonts. var InfoExample = "AaBbCcIiPpQq12369$€¢?.:/()àáâãäåæç日本中国⇧⌘" // Label satisfies the Labeler interface func (fi Info) Label() string { return fi.Family } // Label satisfies the Labeler interface func (fi Family) Label() string { return fi.Family } // Families returns a list of [Family] with one representative per family. func Families(fi []Info) []Family { slices.SortFunc(fi, func(a, b Info) int { return cmp.Compare(a.Family, b.Family) }) n := len(fi) ff := make([]Family, 0, n) for i := 0; i < n; i++ { cur := fi[i].Family ff = append(ff, Family{Family: cur, Example: InfoExample}) for i < n-1 { if fi[i+1].Family != cur { break } i++ } } return ff } // Data contains font information for embedded font data. type Data struct { // Family name. Family string // Weight: normal, bold, etc. Weight rich.Weights // Slant: normal or italic. Slant rich.Slants // Stretch: normal, expanded, condensed, etc. Stretch rich.Stretch // Data contains the font data. Data []byte `display:"-"` // Font contains the loaded font face(s). Fonts []*font.Face } // Load loads the data, setting the Font. func (fd *Data) Load() error { faces, err := font.ParseTTC(bytes.NewReader(fd.Data)) if err != nil { return err } fd.Fonts = faces return nil } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package fonts import ( "fmt" "io/fs" "cogentcore.org/core/base/errors" "github.com/go-text/typesetting/font/opentype" "github.com/go-text/typesetting/fontscan" ) // Embedded are embedded filesystems to get fonts from. By default, // this includes a set of Noto Sans and Roboto Mono fonts. System fonts are // automatically supported separate from this. Use [AddEmbedded] to add // to this. This must be called before the text shaper is created to have an effect. // // On web, Embedded is only used for font metrics, as the actual font // rendering happens through web fonts. See https://cogentcore.org/core/font for // more information. var Embedded = []fs.FS{Default} // AddEmbedded adds to [Embedded] for font loading. func AddEmbedded(fsys ...fs.FS) { Embedded = append(Embedded, fsys...) } // UseEmbeddedInMap adds the fonts from the current [Embedded] list to the given map. func UseEmbeddedInMap(fontMap *fontscan.FontMap) error { return UseInMap(fontMap, Embedded) } // UseInMap adds the fonts from given file systems to the given map. func UseInMap(fontMap *fontscan.FontMap, fss []fs.FS) error { var errs []error for _, fsys := range fss { err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { errs = append(errs, err) return err } if d.IsDir() { return nil } f, err := fsys.Open(path) if err != nil { errs = append(errs, err) return err } defer f.Close() resource, ok := f.(opentype.Resource) if !ok { err = fmt.Errorf("file %q cannot be used as an opentype.Resource", path) errs = append(errs, err) return err } err = fontMap.AddFont(resource, path, "") if err != nil { errs = append(errs, err) return err } return nil }) if err != nil { errs = append(errs, err) } } return errors.Join(errs...) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Command metricsonly extracts font metrics from font files, // discarding all the glyph outlines and other data. package main import ( "fmt" "os" "path/filepath" "cogentcore.org/core/base/errors" "cogentcore.org/core/cli" "github.com/go-text/typesetting/font/opentype" ) //go:generate core generate -add-types -add-funcs func ExtractMetrics(fname, outfile string, debug bool) error { if debug { fmt.Println(fname, "->", outfile) } f, err := os.Open(fname) if err != nil { return err } font, err := opentype.NewLoader(f) if err != nil { return err } // full list from roboto: // GSUB OS/2 STAT cmap gasp glyf head hhea hmtx loca maxp name post prep // minimal effective list: you have to exclude both loca and glyf -- if // you have loca it needs glyf, but otherwise fine to exclude. // include := []string{"head", "hhea", "htmx", "maxp", "name", "cmap"} include := []string{"head", "hhea", "htmx", "maxp", "name", "cmap"} tags := font.Tables() tables := make([]opentype.Table, len(tags)) var taglist []string for i, tag := range tags { if debug { taglist = append(taglist, tag.String()) } skip := true for _, in := range include { if tag.String() == in { skip = false break } } if skip { continue } tables[i].Tag = tag tables[i].Content, err = font.RawTable(tag) if tag.String() == "name" { fmt.Println("name:", string(tables[i].Content)) } } if debug { fmt.Println("\t", taglist) } content := opentype.WriteTTF(tables) return os.WriteFile(outfile, content, 0666) } type Config struct { // Files to extract metrics from. Files []string `flag:"f,files" posarg:"all"` // directory to output the metrics only files. Output string `flag:"output,o"` // emit debug info while processing. todo: use verbose for this! Debug bool `flag:"d,debug"` } // Extract reads fonts and extracts metrics, saving to given output directory. func Extract(c *Config) error { if c.Output != "" { err := os.MkdirAll(c.Output, 0777) if err != nil { return err } } var errs []error for _, fn := range c.Files { _, fname := filepath.Split(fn) outfile := filepath.Join(c.Output, fname) err := ExtractMetrics(fn, outfile, c.Debug) if err != nil { errs = append(errs, err) } } return errors.Join(errs...) } func main() { //types:skip opts := cli.DefaultOptions("metricsonly", "metricsonly extracts font metrics from font files, discarding all the glyph outlines and other data.") cli.Run(opts, &Config{}, Extract) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package fonts import ( "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" "github.com/go-text/typesetting/font" ) // Style sets the [rich.Style] and [text.Style] for the given [font.Face]. func Style(face *font.Face, sty *rich.Style, tsty *text.Style) { if face == nil { return } d := face.Describe() tsty.CustomFont = rich.FontName(d.Family) sty.Family = rich.Custom as := d.Aspect sty.Weight = rich.Weights(int(as.Weight / 100.0)) sty.Slant = rich.Slants(as.Style - 1) // fi[i].Stretch = rich.Stretch() // not avail // fi[i].Stretch = rich.StretchNormal } // 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 ( "log/slog" "strings" "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/text/parse" "cogentcore.org/core/text/parse/lexer" _ "cogentcore.org/core/text/parse/supportedlanguages" "cogentcore.org/core/text/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 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 // Style is the current highlighting style. Style *Style // external toggle to turn off automatic highlighting off bool lastLanguage string lastStyle 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) { if hi.Style == nil { hi.SetStyle(DefaultStyle) } 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 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() lex := hi.parseState.Done().Src.Lexs return lex, 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 } // 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 ( "html" "cogentcore.org/core/text/parse/lexer" ) // maxLineLen prevents overflow in allocating line length const ( maxLineLen = 64 * 1024 maxNumTags = 1024 EscapeHTML = true NoEscapeHTML = false ) // MarkupLineHTML 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 MarkupLineHTML(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.End <= tr.Start { ep := min(sz, ts.End) 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.Start >= sz { break } if tr.Start > cp { mu = append(mu, escf(txt[cp:tr.Start])...) } mu = append(mu, sps...) clsnm := tr.Token.Token.StyleName() mu = append(mu, []byte(clsnm)...) mu = append(mu, sps2...) ep := tr.End addEnd := true if i < nt-1 { if ttags[i+1].Start < tr.End { // next one starts before we end, add to stack addEnd = false ep = ttags[i+1].Start if len(tstack) == 0 { tstack = append(tstack, i) } else { for si := len(tstack) - 1; si >= 0; si-- { ts := ttags[tstack[si]] if tr.End <= ts.End { 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.Start < ep { mu = append(mu, escf(txt[tr.Start: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(html.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(html.EscapeString(string(r))) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package highlighting import ( "fmt" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/runes" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/token" ) // MarkupLineRich returns the [rich.Text] styled line for each tag. // Takes both the hi highlighting tags and extra tags. // The style provides the starting default style properties. func MarkupLineRich(hs *Style, sty *rich.Style, txt []rune, hitags, tags lexer.Line) rich.Text { if len(txt) > maxLineLen { // avoid overflow return rich.NewText(sty, txt[:maxLineLen]) } if hs == nil { return rich.NewText(sty, txt) } sz := len(txt) if sz == 0 { return nil } ttags := lexer.MergeLines(hitags, tags) // ensures that inner-tags are *after* outer tags // fmt.Println(ttags) nt := len(ttags) if nt == 0 || nt > maxNumTags { return rich.NewText(sty, txt) } // first ensure text has spans for each tag region. ln := len(txt) var tx rich.Text cp := 0 for _, tr := range ttags { st := min(tr.Start, ln) if st > cp { tx.AddSpan(sty, txt[cp:st]) cp = st } else if st < cp { tx.SplitSpan(st) } ed := min(tr.End, ln) if ed > cp { tx.AddSpan(sty, txt[cp:ed]) cp = ed } else { tx.SplitSpan(ed) } } if cp < ln { tx.AddSpan(sty, txt[cp:]) } // next, accumulate styles for each span for si := range tx { s, e := tx.Range(si) srng := textpos.Range{Start: s, End: e} cst := *sty for _, tr := range ttags { trng := textpos.Range{Start: tr.Start, End: tr.End} if srng.Intersect(trng).Len() <= 0 { continue } entry := hs.Tag(tr.Token.Token) if !entry.IsZero() { entry.ToRichStyle(&cst) } else { if tr.Token.Token == token.TextSpellErr { cst.SetDecoration(rich.DottedUnderline) // fmt.Println(i, tr) } } } tx.SetSpanStyle(si, &cst) } return tx } // MarkupPathsAsLinks adds hyperlink span styles to given markup of given text, // for any strings that look like file paths / urls. // maxFields is the maximum number of fieldsto look for file paths in: // 2 is a reasonable default, to avoid getting other false-alarm info later. func MarkupPathsAsLinks(txt []rune, mu rich.Text, maxFields int) rich.Text { fl := runes.Fields(txt) mx := min(len(fl), maxFields) for i := range mx { ff := fl[i] if !runes.HasPrefix(ff, []rune("./")) && !runes.HasPrefix(ff, []rune("/")) && !runes.HasPrefix(ff, []rune("../")) { // todo: use regex instead of this. if !runes.Contains(ff, []rune("/")) && !runes.Contains(ff, []rune(":")) { continue } } fi := runes.Index(txt, ff) fnflds := runes.Split(ff, []rune(":")) fn := string(fnflds[0]) pos := "" col := "" if len(fnflds) > 1 { pos = string(fnflds[1]) col = "" if len(fnflds) > 2 { col = string(fnflds[2]) } } url := "" if col != "" { url = fmt.Sprintf(`file:///%v#L%vC%v`, fn, pos, col) } else if pos != "" { url = fmt.Sprintf(`file:///%v#L%v`, fn, pos) } else { url = fmt.Sprintf(`file:///%v`, fn) } si := mu.SplitSpan(fi) efi := fi + len(ff) esi := mu.SplitSpan(efi) sty, _ := mu.Span(si) sty.SetLink(url) mu.SetSpanStyle(si, sty) if esi > 0 { mu.InsertEndSpecial(esi) } else { mu.EndSpecial() } } // if string(mu.Join()) != string(txt) { // panic("markup is not the same: " + string(txt) + " mu: " + string(mu.Join())) // } return mu } // 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/base/fsx" "cogentcore.org/core/colors" "cogentcore.org/core/colors/cam/hct" "cogentcore.org/core/colors/matcolor" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/token" ) type HighlightingName string // 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 "" } // TODO(go1.24): use omitzero instead of omitempty in [StyleEntry] // once we update to go1.24 // StyleEntry is one value in the map of highlight style values type StyleEntry struct { // Color is the text color. Color color.RGBA `json:",omitempty"` // Background color. // In general it is not good to use this because it obscures highlighting. Background color.RGBA `json:",omitempty"` // Border color? not sure what this is -- not really used. Border color.RGBA `display:"-" json:",omitempty"` // Bold font. Bold Trilean `json:",omitempty"` // Italic font. Italic Trilean `json:",omitempty"` // Underline. Underline Trilean `json:",omitempty"` // DottedUnderline DottedUnderline Trilean `json:",omitempty"` // NoInherit indicates to not inherit these settings from sub-category or category levels. // Otherwise everything with a Pass is inherited. NoInherit bool `json:",omitempty"` // themeColor is the theme-adjusted text color. themeColor color.RGBA // themeBackground is the theme-adjusted background color. themeBackground color.RGBA } // // 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.themeColor = 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.themeBackground = 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.DottedUnderline != Pass { out = append(out, se.Underline.Prefix("dotted-underline")) } if se.NoInherit { out = append(out, "noinherit") } if !colors.IsNil(se.themeColor) { out = append(out, colors.AsString(se.themeColor)) } if !colors.IsNil(se.themeBackground) { out = append(out, "bg:"+colors.AsString(se.themeBackground)) } 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.themeColor) { styles = append(styles, "color: "+colors.AsString(se.themeColor)) } if !colors.IsNil(se.themeBackground) { styles = append(styles, "background-color: "+colors.AsString(se.themeBackground)) } 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") } else if se.DottedUnderline == Yes { styles = append(styles, "text-decoration: dotted-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.themeColor) { pr["color"] = se.themeColor } if !colors.IsNil(se.themeBackground) { pr["background-color"] = se.themeBackground } if se.Bold == Yes { pr["font-weight"] = rich.Bold } if se.Italic == Yes { pr["font-style"] = rich.Italic } if se.Underline == Yes { pr["text-decoration"] = 1 << uint32(rich.Underline) } else if se.Underline == Yes { pr["text-decoration"] = 1 << uint32(rich.DottedUnderline) } return pr } // ToRichStyle sets the StyleEntry to given [rich.Style]. func (se StyleEntry) ToRichStyle(sty *rich.Style) { if !colors.IsNil(se.themeColor) { sty.SetFillColor(se.themeColor) } if !colors.IsNil(se.themeBackground) { sty.SetBackground(se.themeBackground) } if se.Bold == Yes { sty.Weight = rich.Bold } if se.Italic == Yes { sty.Slant = rich.Italic } if se.Underline == Yes { sty.Decoration.SetFlag(true, rich.Underline) } else if se.DottedUnderline == Yes { sty.Decoration.SetFlag(true, rich.DottedUnderline) } } // Sub subtracts two style entries, returning an entry with only the differences set func (se StyleEntry) Sub(e StyleEntry) StyleEntry { out := StyleEntry{} if e.Color != se.Color { out.Color = se.Color out.themeColor = se.themeColor } if e.Background != se.Background { out.Background = se.Background out.themeBackground = se.themeBackground } if e.Border != se.Border { out.Border = se.Border } if e.Bold != se.Bold { out.Bold = se.Bold } if e.Italic != se.Italic { out.Italic = se.Italic } if e.Underline != se.Underline { out.Underline = se.Underline } if e.DottedUnderline != se.DottedUnderline { out.DottedUnderline = se.DottedUnderline } return out } // Inherit styles from ancestors. // // Ancestors should be provided from oldest, furthest away to newest, closest. func (se StyleEntry) Inherit(ancestors ...StyleEntry) StyleEntry { out := se for i := len(ancestors) - 1; i >= 0; i-- { if out.NoInherit { return out } ancestor := ancestors[i] if colors.IsNil(out.themeColor) { out.Color = ancestor.Color out.themeColor = ancestor.themeColor } if colors.IsNil(out.themeBackground) { out.Background = ancestor.Background out.themeBackground = ancestor.themeBackground } 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 } if out.DottedUnderline == Pass { out.DottedUnderline = ancestor.DottedUnderline } } return out } func (se StyleEntry) IsZero() bool { return colors.IsNil(se.Color) && colors.IsNil(se.Background) && colors.IsNil(se.Border) && se.Bold == Pass && se.Italic == Pass && se.Underline == Pass && se.DottedUnderline == Pass && !se.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 fsx.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 fsx.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(rich.DottedUnderline), // 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" "slices" "cogentcore.org/core/base/errors" "cogentcore.org/core/base/fsx" "cogentcore.org/core/system" "cogentcore.org/core/text/parse" ) // DefaultStyle is the initial default style. var DefaultStyle = HighlightingName("emacs") // Styles is a collection of styles type Styles map[string]*Style var ( //go:embed defaults.highlighting defaults []byte // StandardStyles are the styles from chroma package StandardStyles Styles // CustomStyles are user's special styles CustomStyles = Styles{} // AvailableStyles are all highlighting styles AvailableStyles Styles // StyleDefault is the default highlighting style name StyleDefault = HighlightingName("emacs") // StyleNames are all the names of all the available highlighting styles StyleNames []string // SettingsStylesFilename is the name of the preferences file in App data // directory for saving / loading the custom styles SettingsStylesFilename = "highlighting.json" // StylesChanged is used for gui updating while editing StylesChanged = false ) // 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 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 fsx.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 fsx.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 } // OpenSettings opens Styles from Cogent Core standard prefs directory, using SettingsStylesFilename func (hs *Styles) OpenSettings() error { pdir := system.TheApp.CogentCoreDataDir() pnm := filepath.Join(pdir, SettingsStylesFilename) StylesChanged = false return hs.OpenJSON(fsx.Filename(pnm)) } // SaveSettings saves Styles to Cogent Core standard prefs directory, using SettingsStylesFilename func (hs *Styles) SaveSettings() error { pdir := system.TheApp.CogentCoreDataDir() pnm := filepath.Join(pdir, SettingsStylesFilename) StylesChanged = false MergeAvailStyles() return hs.SaveJSON(fsx.Filename(pnm)) } // SaveAll saves all styles individually to chosen directory func (hs *Styles) SaveAll(dir fsx.Filename) { for nm, st := range *hs { fnm := filepath.Join(string(dir), nm+".highlighting") st.SaveJSON(fsx.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 { return errors.Log(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++ } slices.Sort(nms) return nms } // 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 ( "sync" "cogentcore.org/core/text/token" "github.com/alecthomas/chroma/v2" ) // FromChroma converts a chroma.TokenType to a parse token.Tokens func TokenFromChroma(ct chroma.TokenType) token.Tokens { chromaToTokensMu.Lock() defer chromaToTokensMu.Unlock() 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] } var ( // chromaToTokensMap maps from chroma.TokenType to Tokens -- built from opposite map chromaToTokensMap map[chroma.TokenType]token.Tokens chromaToTokensMu sync.Mutex ) // 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 htmltext import ( "bytes" "encoding/xml" "fmt" "html" "io" "strings" "unicode" "cogentcore.org/core/base/errors" "cogentcore.org/core/base/stack" "cogentcore.org/core/styles/styleprops" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/runes" "golang.org/x/net/html/charset" ) // HTMLToRich translates HTML-formatted rich text into a [rich.Text], // using given initial text styling parameters and css properties. // This uses the golang XML decoder system, which strips all whitespace // and therefore does not capture any preformatted text. See HTMLPre. // cssProps are a list of css key-value pairs that are used to set styling // properties for the text, and can include class names with a value of // another property map that is applied to elements of that class, // including standard elements like a for links, etc. func HTMLToRich(str []byte, sty *rich.Style, cssProps map[string]any) (rich.Text, error) { sz := len(str) if sz == 0 { return nil, nil } var errs []error 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 // set when a </p> is encountered nextIsParaStart := false // stack of font styles fstack := make(stack.Stack[*rich.Style], 0) fstack.Push(sty) // stack of rich text spans that are later joined for final result spstack := make(stack.Stack[rich.Text], 0) curSp := rich.NewText(sty, nil) spstack.Push(curSp) for { t, err := decoder.Token() if err != nil { if err == io.EOF { break } errs = append(errs, err) break } switch se := t.(type) { case xml.StartElement: fs := fstack.Peek().Clone() // new style for new element atStart := curSp.Len() == 0 if nextIsParaStart && atStart { fs.Decoration.SetFlag(true, rich.ParagraphStart) } nextIsParaStart = false nm := strings.ToLower(se.Name.Local) insertText := []rune{} special := rich.Nothing linkURL := "" if !fs.SetFromHTMLTag(nm) { switch nm { case "a": special = rich.Link fs.SetLinkStyle() for _, attr := range se.Attr { if attr.Name.Local == "href" { linkURL = attr.Value } } case "span": // todo: , "pre" // just uses properties case "q": special = rich.Quote case "math": special = rich.MathInline case "sup": special = rich.Super fs.Size = 0.8 case "sub": special = rich.Sub fs.Size = 0.8 case "dfn": // no default styling case "bdo": // todo: bidirectional override.. case "p": // todo: detect <p> at end of paragraph only fs.Decoration.SetFlag(true, rich.ParagraphStart) case "br": // handled in end: standalone <br> is in both start and end case "err": // custom; used to mark errors default: err := fmt.Errorf("%q tag not recognized", nm) errs = append(errs, err) } } if len(se.Attr) > 0 { sprop := make(map[string]any, len(se.Attr)) for _, attr := range se.Attr { switch attr.Name.Local { case "style": styleprops.FromXMLString(attr.Value, sprop) case "class": if attr.Value == "math inline" { special = rich.MathInline } if attr.Value == "math display" { special = rich.MathDisplay } if cssProps != nil { clnm := "." + attr.Value if aggp, ok := SubProperties(clnm, cssProps); ok { fs.FromProperties(nil, aggp, nil) } } default: sprop[attr.Name.Local] = attr.Value } } fs.FromProperties(nil, sprop, nil) } if cssProps != nil { FontStyleCSS(fs, nm, cssProps) } fstack.Push(fs) if curSp.Len() == 0 && len(spstack) > 0 { // we started something but added nothing to it. spstack.Pop() } if special != rich.Nothing { ss := fs.Clone() // key about specials: make a new one-off style so special doesn't repeat ss.Special = special if special == rich.Link { ss.URL = linkURL } curSp = rich.NewText(ss, insertText) } else { curSp = rich.NewText(fs, insertText) } spstack.Push(curSp) case xml.EndElement: switch se.Name.Local { case "p": curSp.AddRunes([]rune{'\n'}) nextIsParaStart = true case "br": curSp.AddRunes([]rune{'\n'}) nextIsParaStart = false case "a", "q", "math", "sub", "sup": // important: any special must be ended! nsp := rich.Text{} nsp.EndSpecial() spstack.Push(nsp) case "span": sty, stx := curSp.Span(0) if sty.Special != rich.Nothing { if sty.IsMath() { stx = runes.TrimPrefix(stx, []rune("\\(")) stx = runes.TrimSuffix(stx, []rune("\\)")) stx = runes.TrimPrefix(stx, []rune("\\[")) stx = runes.TrimSuffix(stx, []rune("\\]")) // fmt.Println("math:", string(stx)) curSp.SetSpanRunes(0, stx) } nsp := rich.Text{} nsp.EndSpecial() spstack.Push(nsp) } } if len(fstack) > 0 { fstack.Pop() fs := fstack.Peek() curSp = rich.NewText(fs, nil) spstack.Push(curSp) // start a new span with previous style } else { err := fmt.Errorf("imbalanced start / end tags: %q", se.Name.Local) errs = append(errs, err) } case xml.CharData: atStart := curSp.Len() == 0 sstr := html.UnescapeString(string(se)) if nextIsParaStart && atStart { sstr = strings.TrimLeftFunc(sstr, func(r rune) bool { return unicode.IsSpace(r) }) } curSp.AddRunes([]rune(sstr)) } } return rich.Join(spstack...), errors.Join(errs...) } // SubProperties returns a properties map[string]any from given key tag // of given properties map, if the key exists and the value is a sub props map. // Otherwise returns nil, false func SubProperties(tag string, cssProps map[string]any) (map[string]any, bool) { tp, ok := cssProps[tag] if !ok { return nil, false } pmap, ok := tp.(map[string]any) if !ok { return nil, false } return pmap, true } // FontStyleCSS looks for "tag" name properties in cssProps properties, and applies those to // style if found, and returns true -- false if no such tag found func FontStyleCSS(fs *rich.Style, tag string, cssProps map[string]any) bool { if cssProps == nil { return false } pmap, ok := SubProperties(tag, cssProps) if !ok { return false } fs.FromProperties(nil, pmap, nil) 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 htmltext import ( "bytes" "errors" "fmt" "html" "strings" "cogentcore.org/core/base/stack" "cogentcore.org/core/styles/styleprops" "cogentcore.org/core/text/rich" ) // HTMLPreToRich translates preformatted HTML-styled text into a [rich.Text] // using given initial text styling parameters and css properties. // This uses a custom decoder that preserves all whitespace characters, // and decodes all standard inline HTML text style formatting tags in the string. // 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 HTMLPreToRich(str []byte, sty *rich.Style, cssProps map[string]any) (rich.Text, error) { sz := len(str) if sz == 0 { return nil, nil } var errs []error // set when a </p> is encountered nextIsParaStart := false // stack of font styles fstack := make(stack.Stack[*rich.Style], 0) fstack.Push(sty) // stack of rich text spans that are later joined for final result spstack := make(stack.Stack[rich.Text], 0) curSp := rich.NewText(sty, nil) spstack.Push(curSp) tagstack := make(stack.Stack[string], 0) 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 < curSp.AddRunes([]rune(string(str[bidx : bidx+1]))) bidx++ } } if ftag != "" { if ftag[0] == '/' { // EndElement etag := strings.ToLower(ftag[1:]) // fmt.Printf("%v etag: %v\n", bidx, etag) if etag == "pre" { continue // ignore } if etag != curTag { err := fmt.Errorf("end tag: %q doesn't match current tag: %q", etag, curTag) errs = append(errs, err) } switch etag { case "p": curSp.AddRunes([]rune{'\n'}) nextIsParaStart = true case "br": curSp.AddRunes([]rune{'\n'}) nextIsParaStart = false case "a", "q", "math", "sub", "sup": // important: any special must be ended! curSp.EndSpecial() } if len(fstack) > 0 { fstack.Pop() fs := fstack.Peek() curSp = rich.NewText(fs, nil) spstack.Push(curSp) // start a new span with previous style } else { err := fmt.Errorf("imbalanced start / end tags: %q", etag) errs = append(errs, err) } tslen := len(tagstack) if tslen > 1 { tagstack.Pop() curTag = tagstack.Peek() } else if tslen == 1 { tagstack.Pop() curTag = "" } else { err := fmt.Errorf("imbalanced start / end tags: %q", curTag) errs = append(errs, err) } } else { // StartElement 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 fs := fstack.Peek().Clone() // new style for new element atStart := curSp.Len() == 0 if nextIsParaStart && atStart { fs.Decoration.SetFlag(true, rich.ParagraphStart) } nextIsParaStart = false insertText := []rune{} special := rich.Nothing linkURL := "" if !fs.SetFromHTMLTag(stag) { switch stag { case "a": special = rich.Link fs.SetLinkStyle() if nattr > 0 { sprop := make(map[string]any, len(parts)-1) 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" { linkURL = vl } sprop[nm] = vl } } case "span": // just uses properties case "q": special = rich.Quote case "math": special = rich.MathInline case "sup": special = rich.Super fs.Size = 0.8 case "sub": special = rich.Sub fs.Size = 0.8 case "dfn": // no default styling case "bdo": // todo: bidirectional override.. case "pre": // nop case "p": fs.Decoration.SetFlag(true, rich.ParagraphStart) case "br": curSp = rich.NewText(fs, []rune{'\n'}) // br is standalone: do it! spstack.Push(curSp) nextIsParaStart = false default: err := fmt.Errorf("%q tag not recognized", stag) errs = append(errs, err) } } 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": styleprops.FromXMLString(vl, sprop) case "class": if vl == "math inline" { special = rich.MathInline } if vl == "math display" { special = rich.MathDisplay } if cssProps != nil { clnm := "." + vl if aggp, ok := SubProperties(clnm, cssProps); ok { fs.FromProperties(nil, aggp, nil) } } default: sprop[nm] = vl } } fs.FromProperties(nil, sprop, nil) } if cssProps != nil { FontStyleCSS(fs, stag, cssProps) } fstack.Push(fs) curTag = stag tagstack.Push(curTag) if curSp.Len() == 0 && len(spstack) > 0 { // we started something but added nothing to it. spstack.Pop() } if special != rich.Nothing { ss := fs.Clone() // key about specials: make a new one-off style so special doesn't repeat ss.Special = special if special == rich.Link { ss.URL = linkURL } curSp = rich.NewText(ss, insertText) } else { curSp = rich.NewText(fs, insertText) } spstack.Push(curSp) } } else { // raw chars // todo: deal with WhiteSpacePreLine -- trim out non-LF ws 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.AddRunes([]rune(unestr + "\n")) curSp = rich.NewText(fstack.Peek(), nil) spstack.Push(curSp) // start a new span with previous style tmpbuf = tmpbuf[0:0] 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.AddRunes([]rune(unestr)) } } } return rich.Join(spstack...), errors.Join(errs...) } // 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 htmltext import ( "strings" "cogentcore.org/core/text/rich" ) // RichToHTML returns an HTML encoded representation of the rich.Text. func RichToHTML(tx rich.Text) string { var b strings.Builder ns := tx.NumSpans() var lsty *rich.Style for si := range ns { sty, rs := tx.Span(si) var stags, etags string if sty.Weight != rich.Normal && (lsty == nil || lsty.Weight != sty.Weight) { stags += "<" + sty.Weight.HTMLTag() + ">" } else if sty.Weight == rich.Normal && (lsty != nil && lsty.Weight != sty.Weight) { etags += "</" + lsty.Weight.HTMLTag() + ">" } if sty.Slant != rich.SlantNormal && (lsty == nil || lsty.Slant != sty.Slant) { stags += "<i>" } else if sty.Slant == rich.SlantNormal && lsty != nil && lsty.Slant != sty.Slant { etags += "</i>" } if sty.Decoration.HasFlag(rich.Underline) && (lsty == nil || !lsty.Decoration.HasFlag(rich.Underline)) { stags += "<u>" } else if !sty.Decoration.HasFlag(rich.Underline) && lsty != nil && lsty.Decoration.HasFlag(rich.Underline) { etags += "</u>" } b.WriteString(etags) b.WriteString(stags) b.WriteString(string(rs)) lsty = sty } return b.String() } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package lines import ( "image" "regexp" "slices" "cogentcore.org/core/base/errors" "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/text/highlighting" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/search" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/token" ) // this file contains the exported API for Lines // NewLines returns a new empty Lines, with no views. func NewLines() *Lines { ls := &Lines{} ls.Defaults() ls.setText([]byte("")) return ls } // NewLinesFromBytes returns a new Lines representation of given bytes of text, // using given filename to determine the type of content that is represented // in the bytes, based on the filename extension, and given initial display width. // A width-specific view is created, with the unique view id returned: this id // must be used for all subsequent view-specific calls. // This uses all default styling settings. func NewLinesFromBytes(filename string, width int, src []byte) (*Lines, int) { ls := &Lines{} ls.Defaults() fi, _ := fileinfo.NewFileInfo(filename) ls.setFileInfo(fi) _, vid := ls.newView(width) ls.setText(src) return ls, vid } func (ls *Lines) Defaults() { ls.Settings.Defaults() ls.fontStyle = rich.NewStyle().SetFamily(rich.Monospace) ls.links = make(map[int][]rich.Hyperlink) } // NewView makes a new view with given initial width, // with a layout of the existing text at this width. // The return value is a unique int handle that must be // used for all subsequent calls that depend on the view. func (ls *Lines) NewView(width int) int { ls.Lock() defer ls.Unlock() _, vid := ls.newView(width) return vid } // DeleteView deletes view for given unique view id. // It is important to delete unused views to maintain efficient updating of // existing views. func (ls *Lines) DeleteView(vid int) { ls.Lock() defer ls.Unlock() ls.deleteView(vid) } // SetWidth sets the width for line wrapping, for given view id. // If the width is different than current, the layout is updated, // and a true is returned, else false. func (ls *Lines) SetWidth(vid int, wd int) bool { ls.Lock() defer ls.Unlock() vw := ls.view(vid) if vw != nil { if vw.width == wd { return false } vw.width = wd ls.layoutViewLines(vw) // fmt.Println("set width:", vw.width, "lines:", vw.viewLines, "mu:", len(vw.markup), len(vw.vlineStarts)) return true } return false } // Width returns the width for line wrapping for given view id. func (ls *Lines) Width(vid int) int { ls.Lock() defer ls.Unlock() vw := ls.view(vid) if vw != nil { return vw.width } return 0 } // ViewLines returns the total number of line-wrapped view lines, for given view id. func (ls *Lines) ViewLines(vid int) int { ls.Lock() defer ls.Unlock() vw := ls.view(vid) if vw != nil { return vw.viewLines } return 0 } // SetFontStyle sets the font style to use in styling and rendering text. // The Family of the font MUST be set to Monospace. func (ls *Lines) SetFontStyle(fs *rich.Style) *Lines { ls.Lock() defer ls.Unlock() if fs.Family != rich.Monospace { errors.Log(errors.New("lines.Lines font style MUST be Monospace. Setting that but should fix upstream")) fs.Family = rich.Monospace } ls.fontStyle = fs return ls } // FontStyle returns the font style used for this lines. func (ls *Lines) FontStyle() *rich.Style { ls.Lock() defer ls.Unlock() return ls.fontStyle } // SetText sets the text to the given bytes, and does // full markup update and sends a Change event. // Pass nil to initialize an empty lines. func (ls *Lines) SetText(text []byte) *Lines { ls.Lock() ls.setText(text) ls.Unlock() ls.sendChange() return ls } // SetString sets the text to the given string. func (ls *Lines) SetString(txt string) *Lines { return ls.SetText([]byte(txt)) } // SetTextLines sets the source lines from given lines of bytes. func (ls *Lines) SetTextLines(lns [][]byte) { ls.Lock() ls.setLineBytes(lns) ls.Unlock() ls.sendChange() } // Text returns the current text lines as a slice of bytes, // with an additional line feed at the end, per POSIX standards. // It does NOT call EditDone or send a Change event: that should // happen prior or separately from this call. func (ls *Lines) Text() []byte { ls.Lock() defer ls.Unlock() return ls.bytes(0) } // String returns the current text as a string. // It does NOT call EditDone or send a Change event: that should // happen prior or separately from this call. func (ls *Lines) String() string { return string(ls.Text()) } // SetHighlighting sets the highlighting style. func (ls *Lines) SetHighlighting(style highlighting.HighlightingName) { ls.Lock() defer ls.Unlock() ls.Highlighter.SetStyle(style) } // Close should be called when done using the Lines. // It first sends Close events to all views. // An Editor widget will likely want to check IsNotSaved() // and prompt the user to save or cancel first. func (ls *Lines) Close() { ls.sendClose() ls.Lock() ls.stopDelayedReMarkup() ls.views = make(map[int]*view) ls.lines = nil ls.tags = nil ls.hiTags = nil ls.markup = nil // ls.parseState.Reset() // todo ls.undos.Reset() ls.markupEdits = nil ls.posHistory = nil ls.filename = "" ls.notSaved = false ls.Unlock() } // 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 } // SendChange sends an [event.Change] to the views of this lines, // causing them to update. func (ls *Lines) SendChange() { ls.Lock() defer ls.Unlock() ls.sendChange() } // SendInput sends an [event.Input] to the views of this lines, // causing them to update. func (ls *Lines) SendInput() { ls.Lock() defer ls.Unlock() ls.sendInput() } // 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 } ls.Lock() defer ls.Unlock() return ls.isValidLine(ln) } // ValidPos returns a position based on given pos that is valid. func (ls *Lines) ValidPos(pos textpos.Pos) textpos.Pos { ls.Lock() defer ls.Unlock() n := ls.numLines() if n == 0 { return textpos.Pos{} } if pos.Line < 0 { pos.Line = 0 } if pos.Line >= n { pos.Line = n - 1 } llen := len(ls.lines[pos.Line]) if pos.Char < 0 { pos.Char = 0 } if pos.Char > llen { pos.Char = llen // end of line is valid } return pos } // Line returns a (copy of) specific line of runes. func (ls *Lines) Line(ln int) []rune { ls.Lock() defer ls.Unlock() if !ls.isValidLine(ln) { return nil } return slices.Clone(ls.lines[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 source line, in runes. func (ls *Lines) LineLen(ln int) int { ls.Lock() defer ls.Unlock() if !ls.isValidLine(ln) { return 0 } 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 { ls.Lock() defer ls.Unlock() if !ls.isValidLine(ln) { return 0 } 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 { ls.Lock() defer ls.Unlock() if !ls.isValidLine(ln) { return nil } return ls.hiTags[ln] } // LineLexDepth returns the starting lexical depth in terms of brackets, parens, etc func (ls *Lines) LineLexDepth(ln int) int { ls.Lock() defer ls.Unlock() n := len(ls.hiTags) if ln >= n || len(ls.hiTags[ln]) == 0 { return 0 } return ls.hiTags[ln][0].Token.Depth } // EndPos returns the ending position at end of lines. func (ls *Lines) EndPos() textpos.Pos { ls.Lock() defer ls.Unlock() return ls.endPos() } // IsValidPos returns an true if the position is valid. func (ls *Lines) IsValidPos(pos textpos.Pos) bool { ls.Lock() defer ls.Unlock() return ls.isValidPos(pos) } // PosToView returns the view position in terms of ViewLines and Char // offset into that view line for given source line, char position. func (ls *Lines) PosToView(vid int, pos textpos.Pos) textpos.Pos { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.posToView(vw, pos) } // PosFromView returns the original source position from given // view position in terms of ViewLines and Char offset into that view line. // If the Char position is beyond the end of the line, it returns the // end of the given line. func (ls *Lines) PosFromView(vid int, pos textpos.Pos) textpos.Pos { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.posFromView(vw, pos) } // ViewLineLen returns the length in chars (runes) of the given view line. func (ls *Lines) ViewLineLen(vid int, vln int) int { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.viewLineLen(vw, vln) } // ViewLineRegion returns the region in view coordinates of the given view line. func (ls *Lines) ViewLineRegion(vid int, vln int) textpos.Region { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.viewLineRegion(vw, vln) } // ViewLineRegionLocked returns the region in view coordinates of the given view line, // for case where Lines is already locked. func (ls *Lines) ViewLineRegionLocked(vid int, vln int) textpos.Region { vw := ls.view(vid) return ls.viewLineRegion(vw, vln) } // RegionToView converts the given region in source coordinates into view coordinates. func (ls *Lines) RegionToView(vid int, reg textpos.Region) textpos.Region { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.regionToView(vw, reg) } // RegionFromView converts the given region in view coordinates into source coordinates. func (ls *Lines) RegionFromView(vid int, reg textpos.Region) textpos.Region { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.regionFromView(vw, reg) } // 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 textpos.Pos) *textpos.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 textpos.Pos) *textpos.Edit { ls.Lock() defer ls.Unlock() return ls.regionRect(st, ed) } // 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 textpos.Region) textpos.Region { ls.Lock() defer ls.Unlock() return ls.undos.AdjustRegion(reg) } //////// 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. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) DeleteText(st, ed textpos.Pos) *textpos.Edit { ls.Lock() ls.fileModCheck() tbe := ls.deleteText(st, ed) if tbe != nil && ls.Autosave { go ls.autoSave() } ls.Unlock() ls.sendInput() 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.Char >= ed.Char. Sets the timestamp on resulting Edit to now. // An Undo record is automatically saved depending on Undo.Off setting. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) DeleteTextRect(st, ed textpos.Pos) *textpos.Edit { ls.Lock() ls.fileModCheck() tbe := ls.deleteTextRect(st, ed) if tbe != nil && ls.Autosave { go ls.autoSave() } ls.Unlock() ls.sendInput() return tbe } // InsertTextBytes 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. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) InsertTextBytes(st textpos.Pos, text []byte) *textpos.Edit { ls.Lock() ls.fileModCheck() tbe := ls.insertText(st, []rune(string(text))) if tbe != nil && ls.Autosave { go ls.autoSave() } ls.Unlock() ls.sendInput() 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. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) InsertText(st textpos.Pos, text []rune) *textpos.Edit { ls.Lock() ls.fileModCheck() tbe := ls.insertText(st, text) if tbe != nil && ls.Autosave { go ls.autoSave() } ls.Unlock() ls.sendInput() return tbe } // InsertTextLines 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. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) InsertTextLines(st textpos.Pos, text [][]rune) *textpos.Edit { ls.Lock() ls.fileModCheck() tbe := ls.insertTextLines(st, text) if tbe != nil && ls.Autosave { go ls.autoSave() } ls.Unlock() ls.sendInput() 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. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) InsertTextRect(tbe *textpos.Edit) *textpos.Edit { ls.Lock() ls.fileModCheck() tbe = ls.insertTextRect(tbe) if tbe != nil && ls.Autosave { go ls.autoSave() } ls.Unlock() ls.sendInput() return 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. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) ReplaceText(delSt, delEd, insPos textpos.Pos, insTxt string, matchCase bool) *textpos.Edit { ls.Lock() ls.fileModCheck() tbe := ls.replaceText(delSt, delEd, insPos, insTxt, matchCase) if tbe != nil && ls.Autosave { go ls.autoSave() } ls.Unlock() ls.sendInput() return tbe } // AppendTextMarkup appends new text to end of lines, using insert, returns // edit, and uses supplied markup to render it, for preformatted output. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) AppendTextMarkup(text [][]rune, markup []rich.Text) *textpos.Edit { ls.Lock() ls.fileModCheck() tbe := ls.appendTextMarkup(text, markup) if tbe != nil && ls.Autosave { go ls.autoSave() } ls.collectLinks() ls.layoutViews() ls.Unlock() ls.sendInput() return tbe } // ReMarkup starts a background task of redoing the markup func (ls *Lines) ReMarkup() { ls.Lock() defer ls.Unlock() ls.reMarkup() } // SetUndoOn turns on or off the recording of undo records for every edit. func (ls *Lines) SetUndoOn(on bool) { ls.Lock() defer ls.Unlock() ls.undos.Off = !on } // NewUndoGroup increments the undo group counter for batchiung // the subsequent actions. func (ls *Lines) NewUndoGroup() { ls.Lock() defer ls.Unlock() ls.undos.NewGroup() } // UndoReset resets all current undo records. func (ls *Lines) UndoReset() { ls.Lock() defer ls.Unlock() ls.undos.Reset() } // Undo undoes next group of items on the undo stack, // and returns all the edits performed. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) Undo() []*textpos.Edit { ls.Lock() autoSave := ls.batchUpdateStart() tbe := ls.undo() if tbe == nil || ls.undos.Pos == 0 { // no more undo = fully undone ls.clearNotSaved() ls.autosaveDelete() } ls.batchUpdateEnd(autoSave) ls.Unlock() ls.sendInput() return tbe } // Redo redoes next group of items on the undo stack, // and returns all the edits performed. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) Redo() []*textpos.Edit { ls.Lock() autoSave := ls.batchUpdateStart() tbe := ls.redo() ls.batchUpdateEnd(autoSave) ls.Unlock() ls.sendInput() return tbe } // EmacsUndoSave is called by an editor 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() { ls.Lock() defer ls.Unlock() if !ls.Settings.EmacsUndo { return } ls.undos.UndoStackSave() } ///////// Moving // MoveForward moves given source position forward given number of rune steps. func (ls *Lines) MoveForward(pos textpos.Pos, steps int) textpos.Pos { ls.Lock() defer ls.Unlock() return ls.moveForward(pos, steps) } // MoveBackward moves given source position backward given number of rune steps. func (ls *Lines) MoveBackward(pos textpos.Pos, steps int) textpos.Pos { ls.Lock() defer ls.Unlock() return ls.moveBackward(pos, steps) } // MoveForwardWord moves given source position forward given number of word steps. func (ls *Lines) MoveForwardWord(pos textpos.Pos, steps int) textpos.Pos { ls.Lock() defer ls.Unlock() return ls.moveForwardWord(pos, steps) } // MoveBackwardWord moves given source position backward given number of word steps. func (ls *Lines) MoveBackwardWord(pos textpos.Pos, steps int) textpos.Pos { ls.Lock() defer ls.Unlock() return ls.moveBackwardWord(pos, steps) } // MoveDown moves given source position down given number of display line steps, // always attempting to use the given column position if the line is long enough. func (ls *Lines) MoveDown(vid int, pos textpos.Pos, steps, col int) textpos.Pos { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.moveDown(vw, pos, steps, col) } // MoveUp moves given source position up given number of display line steps, // always attempting to use the given column position if the line is long enough. func (ls *Lines) MoveUp(vid int, pos textpos.Pos, steps, col int) textpos.Pos { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.moveUp(vw, pos, steps, col) } // MoveLineStart moves given source position to start of view line. func (ls *Lines) MoveLineStart(vid int, pos textpos.Pos) textpos.Pos { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.moveLineStart(vw, pos) } // MoveLineEnd moves given source position to end of view line. func (ls *Lines) MoveLineEnd(vid int, pos textpos.Pos) textpos.Pos { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.moveLineEnd(vw, pos) } // TransposeChar swaps the character at the cursor with the one before it. func (ls *Lines) TransposeChar(vid int, pos textpos.Pos) bool { ls.Lock() defer ls.Unlock() vw := ls.view(vid) return ls.transposeChar(vw, pos) } //////// Words // IsWordEnd returns true if the cursor is just past the last letter of a word. func (ls *Lines) IsWordEnd(pos textpos.Pos) bool { ls.Lock() defer ls.Unlock() if !ls.isValidPos(pos) { return false } txt := ls.lines[pos.Line] sz := len(txt) if sz == 0 { return false } if pos.Char >= len(txt) { // end of line r := txt[len(txt)-1] return textpos.IsWordBreak(r, -1) } if pos.Char == 0 { // start of line r := txt[0] return !textpos.IsWordBreak(r, -1) } r1 := txt[pos.Char-1] r2 := txt[pos.Char] return !textpos.IsWordBreak(r1, rune(-1)) && textpos.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 (ls *Lines) IsWordMiddle(pos textpos.Pos) bool { ls.Lock() defer ls.Unlock() if !ls.isValidPos(pos) { return false } txt := ls.lines[pos.Line] sz := len(txt) if sz < 2 { return false } if pos.Char >= len(txt) { // end of line return false } if pos.Char == 0 { // start of line return false } r1 := txt[pos.Char-1] r2 := txt[pos.Char] return !textpos.IsWordBreak(r1, rune(-1)) && !textpos.IsWordBreak(r2, rune(-1)) } // WordAt returns a Region for a word starting at given position. // If the current position is a word break then go to next // break after the first non-break. func (ls *Lines) WordAt(pos textpos.Pos) textpos.Region { ls.Lock() defer ls.Unlock() if !ls.isValidPos(pos) { return textpos.Region{} } txt := ls.lines[pos.Line] rng := textpos.WordAt(txt, pos.Char) st := pos st.Char = rng.Start ed := pos ed.Char = rng.End return textpos.NewRegionPos(st, ed) } // WordBefore returns the word before the given source position. // uses IsWordBreak to determine the bounds of the word func (ls *Lines) WordBefore(pos textpos.Pos) *textpos.Edit { ls.Lock() defer ls.Unlock() if !ls.isValidPos(pos) { return &textpos.Edit{} } txt := ls.lines[pos.Line] ch := pos.Char 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 textpos.IsWordBreak(r1, r2) { st = i + 1 break } } if st != ch { return ls.region(textpos.Pos{Line: pos.Line, Char: st}, pos) } return nil } //////// PosHistory // PosHistorySave 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 (ls *Lines) PosHistorySave(pos textpos.Pos) bool { ls.Lock() defer ls.Unlock() if ls.posHistory == nil { ls.posHistory = make([]textpos.Pos, 0, 1000) } sz := len(ls.posHistory) if sz > 0 { if ls.posHistory[sz-1].Line == pos.Line { return false } } ls.posHistory = append(ls.posHistory, pos) // fmt.Printf("saved pos hist: %v\n", pos) return true } // PosHistoryLen returns the length of the position history stack. func (ls *Lines) PosHistoryLen() int { ls.Lock() defer ls.Unlock() return len(ls.posHistory) } // PosHistoryAt returns the position history at given index. // returns false if not a valid index. func (ls *Lines) PosHistoryAt(idx int) (textpos.Pos, bool) { ls.Lock() defer ls.Unlock() if idx < 0 || idx >= len(ls.posHistory) { return textpos.Pos{}, false } return ls.posHistory[idx], true } ///////// Edit helpers // InComment returns true if the given text position is within // a commented region. func (ls *Lines) InComment(pos textpos.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 textpos.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 textpos.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 textpos.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 textpos.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) } //////// Tags // AddTag adds a new custom tag for given line, at given position. func (ls *Lines) AddTag(ln, st, ed int, tag token.Tokens) { ls.Lock() defer ls.Unlock() if !ls.isValidLine(ln) { return } 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 *textpos.Edit, tag token.Tokens) { ls.AddTag(tbe.Region.Start.Line, tbe.Region.Start.Char, tbe.Region.End.Char, tag) } // RemoveTag removes tag (optionally only given tag if non-zero) // at given position if it exists. returns tag. func (ls *Lines) RemoveTag(pos textpos.Pos, tag token.Tokens) (reg lexer.Lex, ok bool) { ls.Lock() defer ls.Unlock() if !ls.isValidLine(pos.Line) { return } ls.tags[pos.Line] = ls.adjustedTags(pos.Line) // re-adjust for current info for i, t := range ls.tags[pos.Line] { if t.ContainsPos(pos.Char) { if tag > 0 && t.Token.Token != tag { continue } ls.tags[pos.Line].DeleteIndex(i) reg = t ok = true break } } if ok { ls.markupLines(pos.Line, pos.Line) } return } // SetTags tags for given line. func (ls *Lines) SetTags(ln int, tags lexer.Line) { ls.Lock() defer ls.Unlock() if !ls.isValidLine(ln) { return } ls.tags[ln] = tags } // 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() } // StopDelayedReMarkup stops the timer for doing markup after an interval. func (ls *Lines) StopDelayedReMarkup() { ls.Lock() defer ls.Unlock() ls.stopDelayedReMarkup() } //////// Misc edit 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. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) IndentLine(ln, ind int) *textpos.Edit { ls.Lock() autoSave := ls.batchUpdateStart() tbe := ls.indentLine(ln, ind) ls.batchUpdateEnd(autoSave) ls.Unlock() ls.sendInput() return tbe } // 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. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) AutoIndent(ln int) (tbe *textpos.Edit, indLev, chPos int) { ls.Lock() autoSave := ls.batchUpdateStart() tbe, indLev, chPos = ls.autoIndent(ln) ls.batchUpdateEnd(autoSave) ls.Unlock() ls.sendInput() return } // AutoIndentRegion does auto-indent over given region; end is *exclusive*. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) AutoIndentRegion(start, end int) { ls.Lock() autoSave := ls.batchUpdateStart() ls.autoIndentRegion(start, end) ls.batchUpdateEnd(autoSave) ls.Unlock() ls.sendInput() } // CommentRegion inserts comment marker on given lines; end is *exclusive*. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) CommentRegion(start, end int) { ls.Lock() autoSave := ls.batchUpdateStart() ls.commentRegion(start, end) ls.batchUpdateEnd(autoSave) ls.Unlock() ls.sendInput() } // 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*. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) JoinParaLines(startLine, endLine int) { ls.Lock() autoSave := ls.batchUpdateStart() ls.joinParaLines(startLine, endLine) ls.batchUpdateEnd(autoSave) ls.Unlock() ls.sendInput() } // TabsToSpaces replaces tabs with spaces over given region; end is *exclusive*. // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) TabsToSpaces(start, end int) { ls.Lock() autoSave := ls.batchUpdateStart() ls.tabsToSpaces(start, end) ls.batchUpdateEnd(autoSave) ls.Unlock() ls.sendInput() } // SpacesToTabs replaces tabs with spaces over given region; end is *exclusive* // Calls sendInput to send an Input event to views, so they update. func (ls *Lines) SpacesToTabs(start, end int) { ls.Lock() autoSave := ls.batchUpdateStart() ls.spacesToTabs(start, end) ls.batchUpdateEnd(autoSave) ls.Unlock() ls.sendInput() } // CountWordsLinesRegion returns the count of words and lines in given region. func (ls *Lines) CountWordsLinesRegion(reg textpos.Region) (words, lines int) { ls.Lock() defer ls.Unlock() words, lines = CountWordsLinesRegion(ls.lines, reg) return } // Diffs computes the diff between this lines and the other lines, // reporting a sequence of operations that would convert this lines (a) into // the other lines (b). Each operation is either an 'r' (replace), 'd' // (delete), 'i' (insert) or 'e' (equal). Everything is line-based (0, offset). func (ls *Lines) Diffs(ob *Lines) Diffs { ls.Lock() defer ls.Unlock() return ls.diffs(ob) } // PatchFrom patches (edits) using content from other, // according to diff operations (e.g., as generated from Diffs). func (ls *Lines) PatchFrom(ob *Lines, diffs Diffs) bool { ls.Lock() defer ls.Unlock() return ls.patchFrom(ob, diffs) } // DiffsUnified computes the diff between this lines and the other lines, // returning a unified diff with given amount of context (default of 3 will be // used if -1) func (ls *Lines) DiffsUnified(ob *Lines, context int) []byte { astr := ls.Strings(true) // needs newlines for some reason bstr := ob.Strings(true) return DiffLinesUnified(astr, bstr, context, ls.Filename(), ls.FileInfo().ModTime.String(), ob.Filename(), ob.FileInfo().ModTime.String()) } //////// 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, []textpos.Match) { ls.Lock() defer ls.Unlock() if lexItems { return search.LexItems(ls.lines, ls.hiTags, find, ignoreCase) } return search.RuneLines(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, []textpos.Match) { ls.Lock() defer ls.Unlock() return search.RuneLinesRegexp(ls.lines, re) } // BraceMatch finds the brace, bracket, or parens that is the partner // of the one at the given position, if there is one of those at this position. func (ls *Lines) BraceMatch(pos textpos.Pos) (textpos.Pos, bool) { ls.Lock() defer ls.Unlock() return ls.braceMatch(pos) } // BraceMatchRune finds the brace, bracket, or parens that is the partner // of the given rune, starting at given position. func (ls *Lines) BraceMatchRune(r rune, pos textpos.Pos) (textpos.Pos, bool) { ls.Lock() defer ls.Unlock() return lexer.BraceMatch(ls.lines, ls.hiTags, r, pos, maxScopeLines) } // LinkAt returns a hyperlink at given source position, if one exists, // nil otherwise. this is fast so no problem to call frequently. func (ls *Lines) LinkAt(pos textpos.Pos) *rich.Hyperlink { ls.Lock() defer ls.Unlock() return ls.linkAt(pos) } // NextLink returns the next hyperlink after given source position, // if one exists, and the line it is on. nil, -1 otherwise. func (ls *Lines) NextLink(pos textpos.Pos) (*rich.Hyperlink, int) { ls.Lock() defer ls.Unlock() return ls.nextLink(pos) } // PrevLink returns the previous hyperlink before given source position, // if one exists, and the line it is on. nil, -1 otherwise. func (ls *Lines) PrevLink(pos textpos.Pos) (*rich.Hyperlink, int) { ls.Lock() defer ls.Unlock() return ls.prevLink(pos) } // Links returns the full list of hyperlinks func (ls *Lines) Links() map[int][]rich.Hyperlink { ls.Lock() defer ls.Unlock() return ls.links } //////// LineColors // SetLineColor sets the color to use for rendering a circle next to the line // number at the given line. func (ls *Lines) SetLineColor(ln int, color image.Image) { ls.Lock() defer ls.Unlock() if ls.lineColors == nil { ls.lineColors = make(map[int]image.Image) } ls.lineColors[ln] = color } // LineColor returns the line color for given line, and bool indicating if set. func (ls *Lines) LineColor(ln int) (image.Image, bool) { ls.Lock() defer ls.Unlock() if ln < 0 { return nil, false } if ls.lineColors == nil { return nil, false } clr, has := ls.lineColors[ln] return clr, has } // DeleteLineColor deletes the line color at the given line. // Passing a -1 clears all current line colors. func (ls *Lines) DeleteLineColor(ln int) { ls.Lock() defer ls.Unlock() if ln < 0 { ls.lineColors = nil return } if ls.lineColors == nil { return } delete(ls.lineColors, ln) } // 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 lines import ( "bytes" "fmt" "strings" "cogentcore.org/core/text/difflib" "cogentcore.org/core/text/textpos" ) // 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 } //////// Lines api // diffs computes the diff between this lines and the other lines, // reporting a sequence of operations that would convert this lines (a) into // the other lines (b). Each operation is either an 'r' (replace), 'd' // (delete), 'i' (insert) or 'e' (equal). Everything is line-based (0, offset). func (ls *Lines) diffs(ob *Lines) Diffs { astr := ls.strings(false) bstr := ob.strings(false) return DiffLines(astr, bstr) } // patchFrom patches (edits) using content from other, // according to diff operations (e.g., as generated from DiffBufs). func (ls *Lines) patchFrom(ob *Lines, diffs Diffs) bool { ls.undos.NewGroup() 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(textpos.Pos{Line: df.I1}, textpos.Pos{Line: df.I2}) ot := ob.Region(textpos.Pos{Line: df.J1}, textpos.Pos{Line: df.J2}) if ot != nil { ls.insertTextLines(textpos.Pos{Line: df.I1}, ot.Text) mods = true } case 'd': ls.deleteText(textpos.Pos{Line: df.I1}, textpos.Pos{Line: df.I2}) mods = true case 'i': ot := ob.Region(textpos.Pos{Line: df.J1}, textpos.Pos{Line: df.J2}) if ot != nil { ln := min(ls.numLines(), df.I1) ls.insertTextLines(textpos.Pos{Line: ln}, ot.Text) mods = true } } } if mods { ls.undos.NewGroup() } return mods } // 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 lines import ( "slices" "cogentcore.org/core/text/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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package lines import "cogentcore.org/core/events" // OnChange adds an event listener function to the view with given // unique id, for the [events.Change] event. // This is used for large-scale changes in the text, such as opening a // new file or setting new text, or EditDone or Save. func (ls *Lines) OnChange(vid int, fun func(e events.Event)) { ls.Lock() defer ls.Unlock() vw := ls.view(vid) if vw != nil { vw.listeners.Add(events.Change, fun) } } // OnInput adds an event listener function to the view with given // unique id, for the [events.Input] event. // This is sent after every fine-grained change in the text, // and is used by text widgets to drive updates. It is blocked // during batchUpdating. func (ls *Lines) OnInput(vid int, fun func(e events.Event)) { ls.Lock() defer ls.Unlock() vw := ls.view(vid) if vw != nil { vw.listeners.Add(events.Input, fun) } } // OnClose adds an event listener function to the view with given // unique id, for the [events.Close] event. // This event is sent in the Close function. func (ls *Lines) OnClose(vid int, fun func(e events.Event)) { ls.Lock() defer ls.Unlock() vw := ls.view(vid) if vw != nil { vw.listeners.Add(events.Close, fun) } } //////// unexported api // sendChange sends a new [events.Change] event to all views listeners. // Must never be called with the mutex lock in place! // This is used to signal that the text has changed, for large-scale changes, // such as opening a new file or setting new text, or EditoDone or Save. func (ls *Lines) sendChange() { e := &events.Base{Typ: events.Change} e.Init() for _, vw := range ls.views { vw.listeners.Call(e) } } // sendInput sends a new [events.Input] event to all views listeners. // Must never be called with the mutex lock in place! // This is used to signal fine-grained changes in the text, // and is used by text widgets to drive updates. It is blocked // during batchUpdating. func (ls *Lines) sendInput() { if ls.batchUpdating { return } e := &events.Base{Typ: events.Input} e.Init() for _, vw := range ls.views { vw.listeners.Call(e) } } // sendClose sends a new [events.Close] event to all views listeners. // Must never be called with the mutex lock in place! // Only sent in the Close function. func (ls *Lines) sendClose() { e := &events.Base{Typ: events.Close} e.Init() for _, vw := range ls.views { vw.listeners.Call(e) } } // 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 (ls *Lines) batchUpdateStart() (autoSave bool) { ls.batchUpdating = true ls.undos.NewGroup() autoSave = ls.autoSaveOff() return } // batchUpdateEnd call to complete BatchUpdateStart func (ls *Lines) batchUpdateEnd(autoSave bool) { ls.autoSaveRestore(autoSave) ls.batchUpdating = 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 lines import ( "io/fs" "log" "log/slog" "os" "path/filepath" "strings" "time" "cogentcore.org/core/base/errors" "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/text/parse" ) //////// exported file api // todo: cleanup and simplify the logic about language support! // Filename returns the current filename func (ls *Lines) Filename() string { ls.Lock() defer ls.Unlock() return ls.filename } // FileInfo returns the current fileinfo func (ls *Lines) FileInfo() *fileinfo.FileInfo { ls.Lock() defer ls.Unlock() return &ls.fileInfo } // ParseState returns the current language properties and ParseState // if it is a parse-supported known language, nil otherwise. // Note: this API will change when LSP is implemented, and current // use is likely to create race conditions / conflicts with markup. func (ls *Lines) ParseState() (*parse.LanguageProperties, *parse.FileStates) { ls.Lock() defer ls.Unlock() lp, _ := parse.LanguageSupport.Properties(ls.parseState.Known) if lp != nil && lp.Lang != nil { return lp, &ls.parseState } return nil, nil } // ParseFileState returns the parsed file state if this is a // parse-supported known language, nil otherwise. // Note: this API will change when LSP is implemented, and current // use is likely to create race conditions / conflicts with markup. func (ls *Lines) ParseFileState() *parse.FileState { ls.Lock() defer ls.Unlock() lp, _ := parse.LanguageSupport.Properties(ls.parseState.Known) if lp != nil && lp.Lang != nil { return ls.parseState.Done() } return nil } // SetFilename sets the filename associated with the buffer and updates // the code highlighting information accordingly. func (ls *Lines) SetFilename(fn string) *Lines { ls.Lock() defer ls.Unlock() return ls.setFilename(fn) } // Stat gets info about the file, including the highlighting language. func (ls *Lines) Stat() error { ls.Lock() defer ls.Unlock() return ls.stat() } // ConfigKnown configures options based on the supported language info in parse. // Returns true if supported. func (ls *Lines) ConfigKnown() bool { ls.Lock() defer ls.Unlock() return ls.configKnown() } // 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) *Lines { ls.Lock() defer ls.Unlock() ls.setFileInfo(info) return ls } // SetFileType sets the syntax highlighting and other parameters // based on the given fileinfo.Known file type func (ls *Lines) SetLanguage(ftyp fileinfo.Known) *Lines { return 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) *Lines { if len(ext) == 0 { return ls } if ext[0] == '.' { ext = ext[1:] } fn := "_fake." + strings.ToLower(ext) fi, _ := fileinfo.NewFileInfo(fn) return ls.SetFileInfo(fi) } // Open loads the given file into the buffer. func (ls *Lines) Open(filename string) error { //types:add ls.Lock() err := ls.openFile(filename) ls.Unlock() ls.sendChange() return err } // OpenFS loads the given file in the given filesystem into the buffer. func (ls *Lines) OpenFS(fsys fs.FS, filename string) error { ls.Lock() err := ls.openFileFS(fsys, filename) ls.Unlock() ls.sendChange() return err } // SaveFile writes current buffer to file, with no prompting, etc func (ls *Lines) SaveFile(filename string) error { ls.Lock() err := ls.saveFile(filename) if err == nil { ls.autosaveDelete() } ls.Unlock() return err } // 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 (ls *Lines) Revert() bool { //types:add ls.Lock() did := ls.revert() ls.Unlock() ls.sendChange() return did } // IsNotSaved returns true if buffer was changed (edited) since last Save. func (ls *Lines) IsNotSaved() bool { ls.Lock() defer ls.Unlock() return ls.notSaved } // ClearNotSaved sets Changed and NotSaved to false. func (ls *Lines) ClearNotSaved() { ls.Lock() defer ls.Unlock() ls.clearNotSaved() } // SetFileModOK sets the flag indicating that it is OK to edit even though // the underlying file on disk has been edited. func (ls *Lines) SetFileModOK(ok bool) { ls.Lock() defer ls.Unlock() ls.fileModOK = ok } // EditDone is called externally (e.g., by Editor widget) when the user // has indicated that editing is done, and the results are to be consumed. func (ls *Lines) EditDone() { ls.Lock() ls.changed = false ls.Unlock() ls.sendChange() } // SetReadOnly sets whether the buffer is read-only. func (ls *Lines) SetReadOnly(readonly bool) *Lines { ls.Lock() defer ls.Unlock() return ls.setReadOnly(readonly) } // AutosaveFilename returns the autosave filename. func (ls *Lines) AutosaveFilename() string { ls.Lock() defer ls.Unlock() return ls.autosaveFilename() } // AutosaveDelete deletes any existing autosave file. func (ls *Lines) AutosaveDelete() { ls.Lock() defer ls.Unlock() ls.autosaveDelete() } // AutosaveCheck checks if an autosave file exists; logic for dealing with // it is left to larger app; call this before opening a file. func (ls *Lines) AutosaveCheck() bool { ls.Lock() defer ls.Unlock() return ls.autosaveCheck() } // 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 (ls *Lines) FileModCheck() bool { ls.Lock() defer ls.Unlock() return ls.fileModCheck() } //////// Unexported implementation // setChanged sets the changed and notSaved flags func (ls *Lines) setChanged() { ls.changed = true ls.notSaved = true } // clearNotSaved sets Changed and NotSaved to false. func (ls *Lines) clearNotSaved() { ls.changed = false ls.notSaved = false } // setReadOnly sets whether the buffer is read-only. // read-only buffers also do not record undo events. func (ls *Lines) setReadOnly(readonly bool) *Lines { ls.readOnly = readonly ls.undos.Off = readonly return ls } // setFilename sets the filename associated with the buffer and updates // the code highlighting information accordingly. func (ls *Lines) setFilename(fn string) *Lines { ls.filename = fn ls.stat() ls.setFileInfo(&ls.fileInfo) return ls } // stat gets info about the file, including the highlighting language. func (ls *Lines) stat() error { ls.fileModOK = false err := ls.fileInfo.InitFile(string(ls.filename)) ls.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 (ls *Lines) configKnown() bool { if ls.fileInfo.Known != fileinfo.Unknown { return ls.Settings.ConfigKnown(ls.fileInfo.Known) } return false } // 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 (ls *Lines) openFile(filename string) error { txt, err := os.ReadFile(string(filename)) if err != nil { return err } ls.setFilename(filename) ls.setText(txt) return nil } // openFileOnly 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 (ls *Lines) openFileOnly(filename string) error { txt, err := os.ReadFile(string(filename)) if err != nil { return err } ls.setFilename(filename) ls.bytesToLines(txt) // not setText! return nil } // openFileFS loads the given file in the given filesystem into the buffer. func (ls *Lines) openFileFS(fsys fs.FS, filename string) error { txt, err := fs.ReadFile(fsys, filename) if err != nil { return err } ls.setFilename(filename) ls.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 (ls *Lines) revert() bool { if ls.filename == "" { return false } ls.stopDelayedReMarkup() ls.autosaveDelete() // justin case didDiff := false if ls.numLines() < diffRevertLines { ob := NewLines() err := ob.openFileOnly(ls.filename) if errors.Log(err) != nil { // sc := tb.sceneFromEditor() // todo: // if sc != nil { // only if viewing // core.ErrorSnackbar(sc, err, "Error reopening file") // } return false } ls.stat() // "own" the new file.. if ob.NumLines() < diffRevertLines { diffs := ls.diffs(ob) if len(diffs) < diffRevertDiffs { ls.patchFrom(ob, diffs) didDiff = true } } } if !didDiff { ls.openFile(ls.filename) } ls.clearNotSaved() ls.autosaveDelete() return true } // saveFile writes current buffer to file, with no prompting, etc func (ls *Lines) saveFile(filename string) error { err := os.WriteFile(string(filename), ls.bytes(0), 0644) if err != nil { // core.ErrorSnackbar(tb.sceneFromEditor(), err) // todo: slog.Error(err.Error()) } else { ls.clearNotSaved() ls.filename = filename ls.stat() } return err } // 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 (ls *Lines) fileModCheck() bool { if ls.filename == "" || ls.fileModOK { return false } info, err := os.Stat(string(ls.filename)) if err != nil { return false } if info.ModTime() != time.Time(ls.fileInfo.ModTime) { if !ls.notSaved { // we haven't edited: just revert ls.revert() return true } if ls.FileModPromptFunc != nil { ls.Unlock() // note: we assume anything getting here will be under lock ls.FileModPromptFunc() ls.Lock() } return true } return false } //////// 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 (ls *Lines) autoSaveOff() bool { asv := ls.Autosave ls.Autosave = false return asv } // autoSaveRestore restores prior Autosave setting, // from AutosaveOff func (ls *Lines) autoSaveRestore(asv bool) { ls.Autosave = asv } // autosaveFilename returns the autosave filename. func (ls *Lines) autosaveFilename() string { path, fn := filepath.Split(ls.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 (ls *Lines) autoSave() error { if ls.autoSaving { return nil } ls.autoSaving = true asfn := ls.autosaveFilename() b := ls.bytes(0) err := os.WriteFile(asfn, b, 0644) if err != nil { log.Printf("Lines: Could not Autosave file: %v, error: %v\n", asfn, err) } ls.autoSaving = false return err } // autosaveDelete deletes any existing autosave file func (ls *Lines) autosaveDelete() { asfn := ls.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 (ls *Lines) autosaveCheck() bool { asfn := ls.autosaveFilename() if _, err := os.Stat(asfn); os.IsNotExist(err) { return false // does not exist } return true } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package lines import ( "unicode" "cogentcore.org/core/base/slicesx" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/textpos" ) // layoutViewLines performs view-specific layout of all lines of current lines markup. // This manages its own memory allocation, so it can be called on a new view. // It must be called after any update to the source text or view layout parameters. func (ls *Lines) layoutViewLines(vw *view) { n := len(ls.markup) if n == 0 { return } vw.markup = vw.markup[:0] vw.vlineStarts = vw.vlineStarts[:0] vw.lineToVline = slicesx.SetLength(vw.lineToVline, n) nln := 0 for ln, mu := range ls.markup { muls, vst := ls.layoutViewLine(ln, vw.width, ls.lines[ln], mu) vw.lineToVline[ln] = len(vw.vlineStarts) vw.markup = append(vw.markup, muls...) vw.vlineStarts = append(vw.vlineStarts, vst...) nln += len(vst) } vw.viewLines = nln } // layoutViewLine performs layout and line wrapping on the given text, // for given view text, with the given markup rich.Text. // The layout is implemented in the markup that is returned. // This clones and then modifies the given markup rich text. func (ls *Lines) layoutViewLine(ln, width int, txt []rune, mu rich.Text) ([]rich.Text, []textpos.Pos) { lt := mu.Clone() n := len(txt) sp := textpos.Pos{Line: ln, Char: 0} // source startinng position vst := []textpos.Pos{sp} // start with this line breaks := []int{} // line break indexes into lt spans clen := 0 // current line length so far start := true prevWasTab := false i := 0 for i < n { r := txt[i] si, sn, ri := lt.Index(i) startOfSpan := sn == ri // fmt.Printf("\n####\n%d\tclen:%d\tsi:%dsn:%d\tri:%d\t%v %v, sisrc: %q txt: %q\n", i, clen, si, sn, ri, startOfSpan, prevWasTab, string(lt[si][ri:]), string(txt[i:min(i+5, n)])) switch { case start && r == '\t': clen += ls.Settings.TabSize if !startOfSpan { lt.SplitSpan(i) // each tab gets its own } prevWasTab = true i++ case r == '\t': tp := (clen / 8) + 1 tp *= 8 clen = tp if !startOfSpan { lt.SplitSpan(i) } prevWasTab = true i++ case unicode.IsSpace(r): start = false clen++ if prevWasTab && !startOfSpan { lt.SplitSpan(i) } prevWasTab = false i++ default: start = false didSplit := false if prevWasTab && !startOfSpan { lt.SplitSpan(i) didSplit = true si++ } prevWasTab = false ns := NextSpace(txt, i) wlen := ns - i // length of word // fmt.Println("word at:", i, "ns:", ns, string(txt[i:ns])) if clen+wlen > width { // need to wrap clen = 0 sp.Char = i vst = append(vst, sp) if !startOfSpan && !didSplit { lt.SplitSpan(i) si++ } breaks = append(breaks, si) if wlen > width { nb := wlen / width if nb*width == wlen { nb-- } bp := i + width for range nb { si, sn, ri := lt.Index(bp) if sn != ri { // not start of span already lt.SplitSpan(bp) si++ } breaks = append(breaks, si) sp.Char = bp vst = append(vst, sp) bp += width } clen = wlen - (nb * width) } } clen += wlen i = ns } } nb := len(breaks) if nb == 0 { return []rich.Text{lt}, vst } muls := make([]rich.Text, 0, nb+1) last := 0 for _, si := range breaks { muls = append(muls, lt[last:si]) last = si } muls = append(muls, lt[last:]) return muls, vst } func NextSpace(txt []rune, pos int) int { n := len(txt) for i := pos; i < n; i++ { r := txt[i] if unicode.IsSpace(r) { return i } } return n } // 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 lines //go:generate core generate -add-types import ( "bytes" "fmt" "image" "log" "slices" "sync" "time" "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/base/metadata" "cogentcore.org/core/base/slicesx" "cogentcore.org/core/text/highlighting" "cogentcore.org/core/text/parse" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/runes" "cogentcore.org/core/text/textpos" ) 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"` // 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"` ) // Lines manages multi-line monospaced text with a given line width in runes, // so that all text wrapping, editing, and navigation logic can be managed // purely in text space, allowing rendering and GUI layout to be relatively fast. // This is suitable for text editing and terminal applications, among others. // The text encoded as runes along with a corresponding [rich.Text] markup // representation with syntax highlighting etc. // The markup is updated in a separate goroutine for efficiency. // Everything is protected by an overall sync.Mutex and is 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 { // Settings are the options for how text editing and viewing works. Settings Settings // Highlighter does the syntax highlighting markup, and contains the // parameters thereof, such as the language and style. Highlighter highlighting.Highlighter // Autosave specifies whether an autosave copy of the file should // be automatically saved after changes are made. Autosave bool // FileModPromptFunc is called when a file has been modified in the filesystem // and it is about to be modified through an edit, in the fileModCheck function. // The prompt should determine whether the user wants to revert, overwrite, or // save current version as a different file. It must block until the user responds. FileModPromptFunc func() // Meta can be used to maintain misc metadata associated with the Lines text, // which allows the Lines object to be the primary data type for applications // dealing with text data, if there are just a few additional data elements needed. // Use standard Go camel-case key names, standards in [metadata]. Meta metadata.Data // fontStyle is the default font styling to use for markup. // Is set to use the monospace font. fontStyle *rich.Style // undos is the undo manager. undos Undo // filename is the filename of the file that was last loaded or saved. // If this is empty then no file-related functionality is engaged. filename string // readOnly marks the contents as not editable. This is for the outer GUI // elements to consult, and is not enforced within Lines itself. readOnly bool // fileInfo is the full information about the current file, if one is set. fileInfo fileinfo.FileInfo // parseState is the parsing state information for the file. parseState parse.FileStates // changed indicates whether any changes have been made. // Use [IsChanged] method to access. changed bool // 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. 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 // markup is the [rich.Text] encoded marked-up version of the text lines, // with the results of syntax highlighting. It just has the raw markup without // additional layout for a specific line width, which goes in a [view]. markup []rich.Text // views are the distinct views of the lines, accessed via a unique view handle, // which is the key in the map. Each view can have its own width, and thus its own // markup and layout. views map[int]*view // lineColors associate a color with a given line number (key of map), // e.g., for a breakpoint or other such function. lineColors map[int]image.Image // markupEdits are the edits that were made during the time it takes to generate // the new markup tags. this is rare but it does happen. markupEdits []*textpos.Edit // markupDelayTimer is the markup delay timer. markupDelayTimer *time.Timer // markupDelayMu is the mutex for updating the markup delay timer. markupDelayMu sync.Mutex // posHistory is the history of cursor positions. // It can be used to move back through them. posHistory []textpos.Pos // links is the collection of all hyperlinks within the markup source, // indexed by the markup source line. // only updated at the full markup sweep. links map[int][]rich.Hyperlink // batchUpdating indicates that a batch update is under way, // so Input signals are not sent until the end. batchUpdating bool // 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 // use Lock(), Unlock() directly for overall mutex on any content updates sync.Mutex } func (ls *Lines) Metadata() *metadata.Data { return &ls.Meta } // 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() } // setText sets the rune lines from source text, // and triggers initial markup and delayed full markup. func (ls *Lines) setText(txt []byte) { ls.bytesToLines(txt) ls.initialMarkup() ls.startDelayedReMarkup() } // bytesToLines sets the rune lines from source text. // it does not trigger any markup but does allocate everything. func (ls *Lines) bytesToLines(txt []byte) { if txt == nil { txt = []byte("") } ls.setLineBytes(bytes.Split(txt, []byte("\n"))) } // setLineBytes sets the lines from source [][]byte. func (ls *Lines) setLineBytes(lns [][]byte) { n := len(lns) if n > 1 && len(lns[n-1]) == 0 { // lines have lf at end typically lns = lns[:n-1] n-- } if ls.fontStyle == nil { ls.Defaults() } 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 lns { ls.lines[ln] = runes.SetFromBytes(ls.lines[ln], txt) ls.markup[ln] = rich.NewText(ls.fontStyle, ls.lines[ln]) // start with raw } } // bytes returns the current text lines as a slice of bytes, up to // given number of lines if maxLines > 0. // Adds an additional line feed at the end, per POSIX standards. func (ls *Lines) bytes(maxLines int) []byte { nl := ls.numLines() if maxLines > 0 { nl = min(nl, maxLines) } nb := 80 * nl b := make([]byte, 0, nb) lastEmpty := false for ln := range nl { ll := []byte(string(ls.lines[ln])) if len(ll) == 0 { lastEmpty = true } b = append(b, ll...) b = append(b, []byte("\n")...) } // https://stackoverflow.com/questions/729692/why-should-text-files-end-with-a-newline if !lastEmpty { b = append(b, []byte("\n")...) } return b } // 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() textpos.Pos { n := ls.numLines() if n == 0 { return textpos.Pos{} } return textpos.Pos{Line: n - 1, Char: len(ls.lines[n-1])} } // appendTextMarkup appends new lines of text to end of lines, // using insert, returns edit, and uses supplied markup to render it. func (ls *Lines) appendTextMarkup(text [][]rune, markup []rich.Text) *textpos.Edit { if len(text) == 0 { return &textpos.Edit{} } text = append(text, []rune{}) ed := ls.endPos() tbe := ls.insertTextLines(ed, text) if tbe == nil { fmt.Println("nil insert", ed, text) return nil } st := tbe.Region.Start.Line el := tbe.Region.End.Line for ln := st; ln < el; ln++ { ls.markup[ln] = markup[ln-st] } return tbe } //////// Edits // validCharPos returns the position with a valid Char position, // if it is not valid. if the line is invalid, it returns false. func (ls *Lines) validCharPos(pos textpos.Pos) (textpos.Pos, bool) { n := ls.numLines() if n == 0 { if pos.Line != 0 { return pos, false } pos.Char = 0 return pos, true } if pos.Line < 0 || pos.Line >= n { return pos, false } llen := len(ls.lines[pos.Line]) if pos.Char < 0 { pos.Char = 0 return pos, true } if pos.Char > llen { pos.Char = llen return pos, true } return pos, true } // isValidPos returns true if position is valid. Note that the end // of the line (at length) is valid. This version does not panic or emit // an error message, and should be used for cases where a position can // legitimately be invalid, and is managed. func (ls *Lines) isValidPos(pos textpos.Pos) bool { n := ls.numLines() if n == 0 { if pos.Line != 0 || pos.Char != 0 { return false } } if pos.Line < 0 || pos.Line >= n { return false } llen := len(ls.lines[pos.Line]) if pos.Char < 0 || pos.Char > llen { return false } return true } // mustValidPos panics if the position is invalid. Note that the end // of the line (at length) is valid. func (ls *Lines) mustValidPos(pos textpos.Pos) { n := ls.numLines() if n == 0 { if pos.Line != 0 || pos.Char != 0 { panic("invalid position for empty text: " + pos.String()) } } if pos.Line < 0 || pos.Line >= n { panic(fmt.Sprintf("invalid line number for n lines %d: pos: %s", n, pos)) } llen := len(ls.lines[pos.Line]) if pos.Char < 0 || pos.Char > llen { panic(fmt.Sprintf("invalid character position for pos, len: %d: pos: %s", llen, pos)) } } // region returns a Edit representation of text between start and end positions // returns nil and logs an error if not a valid region. // sets the timestamp on the Edit to now func (ls *Lines) region(st, ed textpos.Pos) *textpos.Edit { n := ls.numLines() ls.mustValidPos(st) if ed.Line == n && ed.Char == 0 { // end line: goes to endpos ed.Line = n - 1 ed.Char = len(ls.lines[ed.Line]) } ls.mustValidPos(ed) if st == ed { return nil } if !st.IsLess(ed) { log.Printf("lines.region: starting position must be less than ending!: st: %v, ed: %v\n", st, ed) return nil } tbe := &textpos.Edit{Region: textpos.NewRegionPos(st, ed)} if ed.Line == st.Line { sz := ed.Char - st.Char tbe.Text = make([][]rune, 1) tbe.Text[0] = make([]rune, sz) copy(tbe.Text[0][:sz], ls.lines[st.Line][st.Char:ed.Char]) } else { nln := tbe.Region.NumLines() tbe.Text = make([][]rune, nln) stln := st.Line if st.Char > 0 { ec := len(ls.lines[st.Line]) sz := ec - st.Char if sz > 0 { tbe.Text[0] = make([]rune, sz) copy(tbe.Text[0], ls.lines[st.Line][st.Char:]) } stln++ } edln := ed.Line if ed.Char < len(ls.lines[ed.Line]) { tbe.Text[ed.Line-st.Line] = make([]rune, ed.Char) copy(tbe.Text[ed.Line-st.Line], ls.lines[ed.Line][:ed.Char]) edln-- } for ln := stln; ln <= edln; ln++ { ti := ln - st.Line 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 and logs an error if not a valid region. // sets the timestamp on the Edit to now func (ls *Lines) regionRect(st, ed textpos.Pos) *textpos.Edit { ls.mustValidPos(st) ls.mustValidPos(ed) if st == ed { return nil } if !st.IsLess(ed) || st.Char >= ed.Char { log.Printf("core.Buf.RegionRect: starting position must be less than ending!: st: %v, ed: %v\n", st, ed) return nil } tbe := &textpos.Edit{Region: textpos.NewRegionPos(st, ed)} tbe.Rect = true nln := tbe.Region.NumLines() nch := (ed.Char - st.Char) tbe.Text = make([][]rune, nln) for i := range nln { ln := st.Line + i lr := ls.lines[ln] ll := len(lr) var txt []rune if ll > st.Char { sz := min(ll-st.Char, nch) txt = make([]rune, sz, nch) edl := min(ed.Char, ll) copy(txt, lr[st.Char:edl]) } if len(txt) < nch { // rect txt = append(txt, runes.Repeat([]rune(" "), nch-len(txt))...) } tbe.Text[i] = txt } return tbe } // 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 textpos.Pos) *textpos.Edit { tbe := ls.deleteTextImpl(st, ed) ls.saveUndo(tbe) return tbe } func (ls *Lines) deleteTextImpl(st, ed textpos.Pos) *textpos.Edit { tbe := ls.region(st, ed) if tbe == nil { return nil } tbe.Delete = true nl := ls.numLines() if ed.Line == st.Line { if st.Line < nl { ec := min(ed.Char, len(ls.lines[st.Line])) // somehow region can still not be valid. ls.lines[st.Line] = append(ls.lines[st.Line][:st.Char], ls.lines[st.Line][ec:]...) ls.linesEdited(tbe) } } else { // first get chars on start and end stln := st.Line + 1 cpln := st.Line ls.lines[st.Line] = ls.lines[st.Line][:st.Char] eoedl := 0 if ed.Line >= nl { ed.Line = nl - 1 } if ed.Char < len(ls.lines[ed.Line]) { eoedl = len(ls.lines[ed.Line][ed.Char:]) } var eoed []rune if eoedl > 0 { // save it eoed = make([]rune, eoedl) copy(eoed, ls.lines[ed.Line][ed.Char:]) } ls.lines = append(ls.lines[:stln], ls.lines[ed.Line+1:]...) if eoed != nil { ls.lines[cpln] = append(ls.lines[cpln], eoed...) } ls.linesDeleted(tbe) } ls.setChanged() 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.Char >= ed.Char. 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 textpos.Pos) *textpos.Edit { tbe := ls.deleteTextRectImpl(st, ed) ls.saveUndo(tbe) return tbe } func (ls *Lines) deleteTextRectImpl(st, ed textpos.Pos) *textpos.Edit { tbe := ls.regionRect(st, ed) if tbe == nil { return nil } // fmt.Println("del:", tbe.Region) tbe.Delete = true for ln := st.Line; ln <= ed.Line; ln++ { l := ls.lines[ln] // fmt.Println(ln, string(l)) if len(l) > st.Char { if ed.Char <= len(l)-1 { ls.lines[ln] = slices.Delete(l, st.Char, ed.Char) // fmt.Println(ln, "del:", st.Char, ed.Char, string(ls.lines[ln])) } else { ls.lines[ln] = l[:st.Char] // fmt.Println(ln, "trunc", st.Char, ed.Char, string(ls.lines[ln])) } } } ls.linesEdited(tbe) ls.setChanged() 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 textpos.Pos, txt []rune) *textpos.Edit { tbe := ls.insertTextImpl(st, runes.Split(txt, []rune("\n"))) ls.saveUndo(tbe) return tbe } // insertTextLines is the primary method for inserting text, // at given starting position, for text source that is already split // into lines. Do NOT use Impl unless you really don't want to save // the undo: in general very bad not to! // Sets the timestamp on resulting Edit to now. // An Undo record is automatically saved depending on Undo.Off setting. func (ls *Lines) insertTextLines(st textpos.Pos, txt [][]rune) *textpos.Edit { tbe := ls.insertTextImpl(st, txt) ls.saveUndo(tbe) return tbe } // appendLines appends given number of lines at the end. func (ls *Lines) appendLines(n int) { ls.lines = append(ls.lines, make([][]rune, n)...) ls.markup = append(ls.markup, make([]rich.Text, n)...) ls.tags = append(ls.tags, make([]lexer.Line, n)...) ls.hiTags = append(ls.hiTags, make([]lexer.Line, n)...) for _, vw := range ls.views { vw.lineToVline = append(vw.lineToVline, make([]int, n)...) } } // insertTextImpl inserts the Text at given starting position. func (ls *Lines) insertTextImpl(st textpos.Pos, txt [][]rune) *textpos.Edit { if st.Line == ls.numLines() && st.Char == 0 { // adding new line ls.appendLines(1) } ls.mustValidPos(st) nl := len(txt) var tbe *textpos.Edit ed := st if nl == 1 { ls.lines[st.Line] = slices.Insert(ls.lines[st.Line], st.Char, txt[0]...) ed.Char += len(txt[0]) tbe = ls.region(st, ed) ls.linesEdited(tbe) } else { if ls.lines[st.Line] == nil { ls.lines[st.Line] = []rune{} } eostl := len(ls.lines[st.Line][st.Char:]) // end of starting line var eost []rune if eostl > 0 { // save it eost = make([]rune, eostl) copy(eost, ls.lines[st.Line][st.Char:]) } ls.lines[st.Line] = append(ls.lines[st.Line][:st.Char], txt[0]...) nsz := nl - 1 stln := st.Line + 1 ls.lines = slices.Insert(ls.lines, stln, txt[1:]...) ed.Line += nsz ed.Char = len(ls.lines[ed.Line]) if eost != nil { ls.lines[ed.Line] = append(ls.lines[ed.Line], eost...) } tbe = ls.region(st, ed) ls.linesInserted(tbe) } ls.setChanged() 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 *textpos.Edit) *textpos.Edit { re := ls.insertTextRectImpl(tbe) ls.saveUndo(re) return tbe } func (ls *Lines) insertTextRectImpl(tbe *textpos.Edit) *textpos.Edit { st := tbe.Region.Start ed := tbe.Region.End nlns := (ed.Line - st.Line) + 1 if nlns <= 0 { return nil } ls.setChanged() // make sure there are enough lines -- add as needed cln := ls.numLines() if cln <= ed.Line { nln := (1 + ed.Line) - cln tmp := make([][]rune, nln) ls.lines = append(ls.lines, tmp...) ie := &textpos.Edit{} ie.Region.Start.Line = cln - 1 ie.Region.End.Line = ed.Line ls.linesInserted(ie) } nch := (ed.Char - st.Char) for i := 0; i < nlns; i++ { ln := st.Line + i lr := ls.lines[ln] ir := tbe.Text[i] if len(ir) != nch { panic(fmt.Sprintf("insertTextRectImpl: length of rect line: %d, %d != expected from region: %d", i, len(ir), nch)) } if len(lr) < st.Char { lr = append(lr, runes.Repeat([]rune{' '}, st.Char-len(lr))...) } nt := slices.Insert(lr, st.Char, ir...) ls.lines[ln] = nt } re := tbe.Clone() re.Rect = true re.Delete = false re.Region.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 textpos.Pos, insTxt string, matchCase bool) *textpos.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, []rune(insTxt)) } return ls.deleteText(delSt, delEd) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package lines import ( "slices" "time" "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/text/highlighting" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/token" "golang.org/x/exp/maps" ) // 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.parseState.SetSrc(string(info.Path), "", info.Known) ls.Highlighter.Init(info, &ls.parseState) ls.Settings.ConfigKnown(info.Known) if ls.numLines() > 0 { ls.initialMarkup() ls.startDelayedReMarkup() } } // initialMarkup does the first-pass markup on the file func (ls *Lines) initialMarkup() { if !ls.Highlighter.Has || ls.numLines() == 0 { ls.collectLinks() ls.layoutViews() return } txt := ls.bytes(100) if ls.Highlighter.UsingParse() { fs := ls.parseState.Done() // initialize fs.Src.SetBytes(txt) } 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 { ls.collectLinks() ls.layoutViews() 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() } // 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(0) 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() ls.sendInput() } // 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. // For parse-based updates, this is critical for getting full tags // even if there aren't any markupEdits. 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.Region.Start.Line edln := tbe.Region.End.Line pfs.Src.LinesDeleted(stln, edln) } else { stln := tbe.Region.Start.Line + 1 nlns := (tbe.Region.End.Line - tbe.Region.Start.Line) pfs.Src.LinesInserted(stln, nlns) } } for ln := range tags { // todo: something weird about this -- not working in test tags[ln] = pfs.LexLine(ln) // does clone, combines comments too } } else { for _, tbe := range edits { if tbe.Delete { stln := tbe.Region.Start.Line edln := tbe.Region.End.Line tags = append(tags[:stln], tags[edln:]...) } else { stln := tbe.Region.Start.Line + 1 nlns := (tbe.Region.End.Line - tbe.Region.Start.Line) 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) mu := highlighting.MarkupLineRich(ls.Highlighter.Style, ls.fontStyle, ls.lines[ln], tags[ln], ls.tags[ln]) ls.markup[ln] = mu } ls.collectLinks() ls.layoutViews() } // collectLinks finds all the links in markup into links. func (ls *Lines) collectLinks() { ls.links = make(map[int][]rich.Hyperlink) for ln, mu := range ls.markup { lks := mu.GetLinks() if len(lks) > 0 { ls.links[ln] = lks } } } // layoutViews updates layout of all view lines. func (ls *Lines) layoutViews() { for _, vw := range ls.views { ls.layoutViewLines(vw) } } // 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) var mu rich.Text if err == nil { ls.hiTags[ln] = mt mu = highlighting.MarkupLineRich(ls.Highlighter.Style, ls.fontStyle, ltxt, mt, ls.adjustedTags(ln)) lks := mu.GetLinks() if len(lks) > 0 { ls.links[ln] = lks } } else { mu = rich.NewText(ls.fontStyle, ltxt) allgood = false } ls.markup[ln] = mu } for _, vw := range ls.views { ls.layoutViewLines(vw) } // Now we trigger a background reparse of everything in a separate parse.FilesState // that gets switched into the current. return allgood } //////// Lines and tags // linesEdited re-marks-up lines in edit (typically only 1). func (ls *Lines) linesEdited(tbe *textpos.Edit) { if tbe == nil { return } st, ed := tbe.Region.Start.Line, tbe.Region.End.Line for ln := st; ln <= ed; ln++ { ls.markup[ln] = rich.NewText(ls.fontStyle, 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 *textpos.Edit) { stln := tbe.Region.Start.Line + 1 nsz := (tbe.Region.End.Line - tbe.Region.Start.Line) ls.markupEdits = append(ls.markupEdits, tbe) if nsz > 0 { ls.markup = slices.Insert(ls.markup, stln, make([]rich.Text, nsz)...) ls.tags = slices.Insert(ls.tags, stln, make([]lexer.Line, nsz)...) ls.hiTags = slices.Insert(ls.hiTags, stln, make([]lexer.Line, nsz)...) for _, vw := range ls.views { vw.lineToVline = slices.Insert(vw.lineToVline, stln, make([]int, 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 *textpos.Edit) { ls.markupEdits = append(ls.markupEdits, tbe) stln := tbe.Region.Start.Line edln := tbe.Region.End.Line if edln > stln { 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) } } // remarkup of start line: st := tbe.Region.Start.Line ls.markupLines(st, st) ls.startDelayedReMarkup() } // 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 := textpos.Region{Start: textpos.Pos{Line: ln, Char: tg.Start}, End: textpos.Pos{Line: ln, Char: tg.End}} reg.Time = tg.Time reg = ls.undos.AdjustRegion(reg) if !reg.IsNil() { ntr := ntags.AddLex(tg.Token, reg.Start.Char, reg.End.Char) ntr.Time.Now() } } return ntags } // 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.End > lln { return "" } stlx := lexer.ObjPathAt(ls.hiTags[ln], lx) if stlx.Start >= lx.End { return "" } return string(ls.lines[ln][stlx.Start:lx.End]) } // 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 textpos.Pos) (*lexer.Lex, int) { if !ls.isValidLine(pos.Line) { return nil, -1 } return ls.hiTags[pos.Line].AtPos(pos.Char) } // inTokenSubCat returns true if the given text position is marked with lexical // type in given SubCat sub-category. func (ls *Lines) inTokenSubCat(pos textpos.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 textpos.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 textpos.Pos) bool { lx, _ := ls.hiTagAtPos(pos) if lx == nil { return false } return lx.Token.Token.IsCode() } func (ls *Lines) braceMatch(pos textpos.Pos) (textpos.Pos, bool) { if !ls.isValidPos(pos) { return textpos.Pos{}, false } txt := ls.lines[pos.Line] ch := pos.Char if ch >= len(txt) { return textpos.Pos{}, false } r := txt[ch] if r == '{' || r == '}' || r == '(' || r == ')' || r == '[' || r == ']' { return lexer.BraceMatch(ls.lines, ls.hiTags, r, pos, maxScopeLines) } return textpos.Pos{}, false } // linkAt returns a hyperlink at given source position, if one exists, // nil otherwise. this is fast so no problem to call frequently. func (ls *Lines) linkAt(pos textpos.Pos) *rich.Hyperlink { ll := ls.links[pos.Line] if len(ll) == 0 { return nil } for _, l := range ll { if l.Range.Contains(pos.Char) { return &l } } return nil } // nextLink returns the next hyperlink after given source position, // if one exists, and the line it is on. nil, -1 otherwise. func (ls *Lines) nextLink(pos textpos.Pos) (*rich.Hyperlink, int) { cl := ls.linkAt(pos) if cl != nil { pos.Char = cl.Range.End } ll := ls.links[pos.Line] for _, l := range ll { if l.Range.Contains(pos.Char) { return &l, pos.Line } } // find next line lns := maps.Keys(ls.links) slices.Sort(lns) for _, ln := range lns { if ln <= pos.Line { continue } l := &ls.links[ln][0] return l, ln } return nil, -1 } // prevLink returns the previous hyperlink before given source position, // if one exists, and the line it is on. nil, -1 otherwise. func (ls *Lines) prevLink(pos textpos.Pos) (*rich.Hyperlink, int) { cl := ls.linkAt(pos) if cl != nil { if cl.Range.Start == 0 { pos = ls.moveBackward(pos, 1) } else { pos.Char = cl.Range.Start - 1 } } ll := ls.links[pos.Line] for _, l := range ll { if l.Range.End <= pos.Char { return &l, pos.Line } } // find prev line lns := maps.Keys(ls.links) slices.Sort(lns) nl := len(lns) for i := nl - 1; i >= 0; i-- { ln := lns[i] if ln >= pos.Line { continue } return &ls.links[ln][0], ln } return nil, -1 } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package lines import ( "cogentcore.org/core/text/textpos" ) // moveForward moves given source position forward given number of rune steps. func (ls *Lines) moveForward(pos textpos.Pos, steps int) textpos.Pos { if !ls.isValidPos(pos) { return pos } for range steps { pos.Char++ llen := len(ls.lines[pos.Line]) if pos.Char > llen { if pos.Line < len(ls.lines)-1 { pos.Char = 0 pos.Line++ } else { pos.Char = llen break } } } return pos } // moveBackward moves given source position backward given number of rune steps. func (ls *Lines) moveBackward(pos textpos.Pos, steps int) textpos.Pos { if !ls.isValidPos(pos) { return pos } for range steps { pos.Char-- if pos.Char < 0 { if pos.Line > 0 { pos.Line-- pos.Char = len(ls.lines[pos.Line]) } else { pos.Char = 0 break } } } return pos } // moveForwardWord moves given source position forward given number of word steps. func (ls *Lines) moveForwardWord(pos textpos.Pos, steps int) textpos.Pos { if !ls.isValidPos(pos) { return pos } nstep := 0 for nstep < steps { op := pos.Char np, ns := textpos.ForwardWord(ls.lines[pos.Line], op, steps) nstep += ns pos.Char = np if np == op || pos.Line >= len(ls.lines)-1 { break } if nstep < steps { pos.Line++ pos.Char = 0 } } return pos } // moveBackwardWord moves given source position backward given number of word steps. func (ls *Lines) moveBackwardWord(pos textpos.Pos, steps int) textpos.Pos { if !ls.isValidPos(pos) { return pos } nstep := 0 for nstep < steps { op := pos.Char np, ns := textpos.BackwardWord(ls.lines[pos.Line], op, steps) nstep += ns pos.Char = np if pos.Line == 0 { break } if nstep < steps { pos.Line-- pos.Char = len(ls.lines[pos.Line]) } } return pos } // moveDown moves given source position down given number of display line steps, // always attempting to use the given column position if the line is long enough. func (ls *Lines) moveDown(vw *view, pos textpos.Pos, steps, col int) textpos.Pos { if !ls.isValidPos(pos) { return pos } vl := vw.viewLines vp := ls.posToView(vw, pos) nvp := vp nvp.Line = min(nvp.Line+steps, vl-1) nvp.Char = col dp := ls.posFromView(vw, nvp) return dp } // moveUp moves given source position up given number of display line steps, // always attempting to use the given column position if the line is long enough. func (ls *Lines) moveUp(vw *view, pos textpos.Pos, steps, col int) textpos.Pos { if !ls.isValidPos(pos) { return pos } vp := ls.posToView(vw, pos) nvp := vp nvp.Line = max(nvp.Line-steps, 0) nvp.Char = col dp := ls.posFromView(vw, nvp) return dp } // moveLineStart moves given source position to start of view line. func (ls *Lines) moveLineStart(vw *view, pos textpos.Pos) textpos.Pos { if !ls.isValidPos(pos) { return pos } vp := ls.posToView(vw, pos) vp.Char = 0 return ls.posFromView(vw, vp) } // moveLineEnd moves given source position to end of view line. func (ls *Lines) moveLineEnd(vw *view, pos textpos.Pos) textpos.Pos { if !ls.isValidPos(pos) { return pos } vp := ls.posToView(vw, pos) vp.Char = ls.viewLineLen(vw, vp.Line) return ls.posFromView(vw, vp) } // transposeChar swaps the character at the cursor with the one before it. func (ls *Lines) transposeChar(vw *view, pos textpos.Pos) bool { if !ls.isValidPos(pos) { return false } vp := ls.posToView(vw, pos) pvp := vp pvp.Char-- if pvp.Char < 0 { return false } ppos := ls.posFromView(vw, pvp) chr := ls.lines[pos.Line][pos.Char] pchr := ls.lines[ppos.Line][ppos.Char] repl := string([]rune{chr, pchr}) pos.Char++ ls.replaceText(ppos, pos, ppos, repl, ReplaceMatchCase) return true } // 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 lines import ( "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/base/indent" "cogentcore.org/core/text/parse" "cogentcore.org/core/text/text" ) // Settings contains settings for editing text lines. type Settings struct { text.EditorSettings // CommentLine are character(s) that start a single-line comment; // if empty then multi-line comment syntax will be used. CommentLine string // CommentStart are character(s) that start a multi-line comment // or one that requires both start and end. CommentStart string // Commentend are 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 *Settings) 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 *Settings) 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 *Settings) 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 lines import ( "fmt" "runtime/debug" "sync" "time" "cogentcore.org/core/text/textpos" ) // 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 []*textpos.Edit // undo stack of *undo* edits -- added to whenever an Undo is done -- for emacs-style undo UndoStack []*textpos.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 *textpos.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.Region.Since(&un.Stack[len(un.Stack)-1].Region) 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() *textpos.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.Region, 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) *textpos.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.Region, 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 *textpos.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() *textpos.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.Region, 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) *textpos.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.Region, 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 textpos.Region) textpos.Region { if un.Off { return reg } un.Mu.Lock() defer un.Mu.Unlock() for _, utbe := range un.Stack { reg = utbe.AdjustRegion(reg) if reg == (textpos.Region{}) { return reg } } return reg } //////// Lines api // saveUndo saves given edit to undo stack. func (ls *Lines) saveUndo(tbe *textpos.Edit) { if tbe == nil { return } ls.undos.Save(tbe) } // undo undoes next group of items on the undo stack func (ls *Lines) undo() []*textpos.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 []*textpos.Edit for { if tbe.Rect { if tbe.Delete { utbe := ls.insertTextRectImpl(tbe) utbe.Group = stgp + tbe.Group if ls.Settings.EmacsUndo { ls.undos.SaveUndo(utbe) } eds = append(eds, utbe) } else { utbe := ls.deleteTextRectImpl(tbe.Region.Start, tbe.Region.End) utbe.Group = stgp + tbe.Group if ls.Settings.EmacsUndo { ls.undos.SaveUndo(utbe) } eds = append(eds, utbe) } } else { if tbe.Delete { utbe := ls.insertTextImpl(tbe.Region.Start, tbe.Text) utbe.Group = stgp + tbe.Group if ls.Settings.EmacsUndo { ls.undos.SaveUndo(utbe) } eds = append(eds, utbe) } else { if !ls.isValidPos(tbe.Region.End) { fmt.Println("lines.undo: invalid end region for undo. stack:", len(ls.undos.Stack), "tbe:", tbe) debug.PrintStack() break } utbe := ls.deleteTextImpl(tbe.Region.Start, tbe.Region.End) utbe.Group = stgp + tbe.Group if ls.Settings.EmacsUndo { ls.undos.SaveUndo(utbe) } eds = append(eds, utbe) } } tbe = ls.undos.UndoPopIfGroup(stgp) if tbe == nil { break } } return eds } // redo redoes next group of items on the undo stack, // and returns the last record, nil if no more func (ls *Lines) redo() []*textpos.Edit { tbe := ls.undos.RedoNext() if tbe == nil { return nil } var eds []*textpos.Edit stgp := tbe.Group for { if tbe.Rect { if tbe.Delete { ls.deleteTextRectImpl(tbe.Region.Start, tbe.Region.End) } else { ls.insertTextRectImpl(tbe) } } else { if tbe.Delete { ls.deleteTextImpl(tbe.Region.Start, tbe.Region.End) } else { ls.insertTextImpl(tbe.Region.Start, tbe.Text) } } eds = append(eds, tbe) tbe = ls.undos.RedoNextIfGroup(stgp) if tbe == nil { break } } return eds } // 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 lines import ( "bufio" "bytes" "io" "log/slog" "os" "strings" "cogentcore.org/core/base/indent" "cogentcore.org/core/text/parse" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/runes" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/token" ) // 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.Line - Start.Line) func CountWordsLinesRegion(src [][]rune, reg textpos.Region) (words, lines int) { lns := len(src) mx := min(lns-1, reg.End.Line) for ln := reg.Start.Line; ln <= mx; ln++ { sln := src[ln] if ln == reg.Start.Line { sln = sln[reg.Start.Char:] } else if ln == reg.End.Line { sln = sln[:reg.End.Char] } flds := strings.Fields(string(sln)) words += len(flds) } lines = 1 + (reg.End.Line - reg.Start.Line) 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 } //////// 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) *textpos.Edit { tabSz := ls.Settings.TabSize ichr := indent.Tab if ls.Settings.SpaceIndent { ichr = indent.Space } curind, _ := lexer.LineIndent(ls.lines[ln], tabSz) if ind > curind { txt := runes.SetFromBytes([]rune{}, indent.Bytes(ichr, ind-curind, tabSz)) return ls.insertText(textpos.Pos{Line: ln}, txt) } else if ind < curind { spos := indent.Len(ichr, ind, tabSz) cpos := indent.Len(ichr, curind, tabSz) return ls.deleteText(textpos.Pos{Line: ln, Char: spos}, textpos.Pos{Line: ln, Char: 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 *textpos.Edit, indLev, chPos int) { tabSz := ls.Settings.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.Settings.IndentChar() indLev = max(pInd+delInd, 0) 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.Settings.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 textpos.Pos) bool { if ls.inTokenSubCat(pos, token.Comment) { return true } cs := ls.commentStart(pos.Line) if cs < 0 { return false } return pos.Char > 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.Settings.TabSize ch := 0 ind, _ := lexer.LineIndent(ls.lines[start], tabSz) if ind > 0 { if ls.Settings.SpaceIndent { ch = ls.Settings.TabSize * ind } else { ch = ind } } comst, comed := ls.Settings.CommentStrings() if comst == "" { // log.Printf("text.Lines: attempt to comment region without any comment syntax defined") comst = "// " 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 } rcomst := []rune(comst) rcomed := []rune(comed) for ln := start; ln < eln; ln++ { if doCom { ipos, ok := ls.validCharPos(textpos.Pos{Line: ln, Char: ch}) if ok { ls.insertText(ipos, rcomst) if comed != "" { lln := len(ls.lines[ln]) // automatically ok ls.insertText(textpos.Pos{Line: ln, Char: lln}, rcomed) } } } else { idx := ls.commentStart(ln) if idx >= 0 { ls.deleteText(textpos.Pos{Line: ln, Char: idx}, textpos.Pos{Line: ln, Char: idx + len(comst)}) } if comed != "" { idx := runes.IndexFold(ls.lines[ln], []rune(comed)) if idx >= 0 { ls.deleteText(textpos.Pos{Line: ln, Char: idx}, textpos.Pos{Line: ln, Char: 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 lr := ls.lines[ln] lrt := runes.TrimSpace(lr) if len(lrt) == 0 || ln == startLine { if ln < curEd-1 { stp := textpos.Pos{Line: ln + 1} if ln == startLine { stp.Line-- } ep := textpos.Pos{Line: curEd - 1} if curEd == endLine { ep.Line = curEd } eln := ls.lines[ep.Line] ep.Char = len(eln) trt := runes.Join(ls.lines[stp.Line:ep.Line+1], []rune(" ")) ls.replaceText(stp, ep, stp, string(trt), ReplaceNoMatchCase) } curEd = ln } } } // tabsToSpacesLine replaces tabs with spaces in the given line. func (ls *Lines) tabsToSpacesLine(ln int) { tabSz := ls.Settings.TabSize lr := ls.lines[ln] st := textpos.Pos{Line: ln} ed := textpos.Pos{Line: ln} i := 0 for { if i >= len(lr) { break } r := lr[i] if r == '\t' { po := i % tabSz nspc := tabSz - po st.Char = i ed.Char = 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.Settings.TabSize lr := ls.lines[ln] st := textpos.Pos{Line: ln} ed := textpos.Pos{Line: ln} i := 0 nspc := 0 for { if i >= len(lr) { break } r := lr[i] if r == ' ' { nspc++ if nspc == tabSz { st.Char = i - (tabSz - 1) ed.Char = 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) } } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package lines import ( "cogentcore.org/core/events" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/textpos" ) // view provides a view onto a shared [Lines] text buffer, // with a representation of view lines that are the wrapped versions of // the original [Lines.lines] source lines, with wrapping according to // the view width. Views are managed by the Lines. type view struct { // width is the current line width in rune characters, used for line wrapping. width int // viewLines is the total number of line-wrapped lines. viewLines int // vlineStarts are the positions in the original [Lines.lines] source for // the start of each view line. This slice is viewLines in length. vlineStarts []textpos.Pos // markup is the view-specific version of the [Lines.markup] markup for // each view line (len = viewLines). markup []rich.Text // lineToVline maps the source [Lines.lines] indexes to the wrapped // viewLines. Each slice value contains the index into the viewLines space, // such that vlineStarts of that index is the start of the original source line. // Any subsequent vlineStarts with the same Line and Char > 0 following this // starting line represent additional wrapped content from the same source line. lineToVline []int // listeners is used for sending Change, Input, and Close events to views. listeners events.Listeners } // viewLineLen returns the length in chars (runes) of the given view line. func (ls *Lines) viewLineLen(vw *view, vl int) int { n := len(vw.vlineStarts) if n == 0 { return 0 } if vl < 0 { vl = 0 } if vl >= n { vl = n - 1 } vs := vw.vlineStarts[vl] sl := ls.lines[vs.Line] if vl == vw.viewLines-1 { return len(sl) + 1 - vs.Char } np := vw.vlineStarts[vl+1] if np.Line == vs.Line { return np.Char - vs.Char } return len(sl) + 1 - vs.Char } // viewLinesRange returns the start and end view lines for given // source line number, using only lineToVline. ed is inclusive. func (ls *Lines) viewLinesRange(vw *view, ln int) (st, ed int) { n := len(vw.lineToVline) st = vw.lineToVline[ln] if ln+1 < n { ed = vw.lineToVline[ln+1] - 1 } else { ed = vw.viewLines - 1 } return } // validViewLine returns a view line that is in range based on given // source line. func (ls *Lines) validViewLine(vw *view, ln int) int { if ln < 0 { return 0 } else if ln >= len(vw.lineToVline) { return vw.viewLines - 1 } return vw.lineToVline[ln] } // posToView returns the view position in terms of viewLines and Char // offset into that view line for given source line, char position. // Is robust to out-of-range positions. func (ls *Lines) posToView(vw *view, pos textpos.Pos) textpos.Pos { vp := pos vl := ls.validViewLine(vw, pos.Line) vp.Line = vl vlen := ls.viewLineLen(vw, vl) if pos.Char < vlen { return vp } nl := vl + 1 if nl == vw.viewLines { vp.Char = pos.Char return vp } for nl < vw.viewLines && vw.vlineStarts[nl].Line == pos.Line { np := vw.vlineStarts[nl] vlen := ls.viewLineLen(vw, nl) if pos.Char >= np.Char && pos.Char < np.Char+vlen { np.Line = nl np.Char = pos.Char - np.Char return np } nl++ } return vp } // posFromView returns the original source position from given // view position in terms of viewLines and Char offset into that view line. // If the Char position is beyond the end of the line, it returns the // end of the given line. func (ls *Lines) posFromView(vw *view, vp textpos.Pos) textpos.Pos { n := len(vw.vlineStarts) if n == 0 { return textpos.Pos{} } vl := vp.Line if vl < 0 { vl = 0 } else if vl >= n { vl = n - 1 } vlen := ls.viewLineLen(vw, vl) if vlen == 0 { vlen = 1 } vp.Char = min(vp.Char, vlen-1) pos := vp sp := vw.vlineStarts[vl] pos.Line = sp.Line pos.Char = sp.Char + vp.Char return pos } // regionToView converts the given region in source coordinates into view coordinates. func (ls *Lines) regionToView(vw *view, reg textpos.Region) textpos.Region { return textpos.Region{Start: ls.posToView(vw, reg.Start), End: ls.posToView(vw, reg.End)} } // regionFromView converts the given region in view coordinates into source coordinates. func (ls *Lines) regionFromView(vw *view, reg textpos.Region) textpos.Region { return textpos.Region{Start: ls.posFromView(vw, reg.Start), End: ls.posFromView(vw, reg.End)} } // viewLineRegion returns the region in view coordinates of the given view line. func (ls *Lines) viewLineRegion(vw *view, vln int) textpos.Region { llen := ls.viewLineLen(vw, vln) return textpos.Region{Start: textpos.Pos{Line: vln}, End: textpos.Pos{Line: vln, Char: llen}} } // initViews ensures that the views map is constructed. func (ls *Lines) initViews() { if ls.views == nil { ls.views = make(map[int]*view) } } // view returns view for given unique view id. nil if not found. func (ls *Lines) view(vid int) *view { ls.initViews() return ls.views[vid] } // newView makes a new view with next available id, using given initial width. func (ls *Lines) newView(width int) (*view, int) { ls.initViews() mxi := 0 for i := range ls.views { mxi = max(i, mxi) } id := mxi + 1 vw := &view{width: width} ls.views[id] = vw ls.layoutViewLines(vw) return vw, id } // deleteView deletes view with given view id. func (ls *Lines) deleteView(vid int) { delete(ls.views, vid) } // ViewMarkupLine returns the markup [rich.Text] line for given view and // view line number. This must be called under the mutex Lock! It is the // api for rendering the lines. func (ls *Lines) ViewMarkupLine(vid, line int) rich.Text { vw := ls.view(vid) if line >= 0 && len(vw.markup) > line { return vw.markup[line] } return rich.Text{} } // 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/text/parse" _ "cogentcore.org/core/text/parse/languages" "cogentcore.org/core/text/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/text/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/text/parse/lexer" "cogentcore.org/core/text/parse/parser" "cogentcore.org/core/text/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; [lines.Lines] 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.Line >= 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 ( "errors" "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 "", errors.New("BIBINPUTS and TEXINPUTS variables are empty") } 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 { ch := s.read() if ch == eof { break } if !isAlphanum(ch) && !isBareSymbol(ch) || isWhitespace(ch) { s.unread() break } _, _ = 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/text/parse" "cogentcore.org/core/text/parse/complete" "cogentcore.org/core/text/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/text/parse" "cogentcore.org/core/text/parse/complete" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/parse/parser" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/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 textpos.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.Start = 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.Start.Line, mt.Region.End.Line) return } } } // fmt.Printf("got lookup type: %v, last str: %v\n", typ.String(), lststr) ld.SetFile(typ.Filename, typ.Region.Start.Line, typ.Region.End.Line) 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 textpos.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.Start = 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 textpos.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.Start.Line, psy.Region.End.Line) 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.Start.Line, tym.Region.End.Line) 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.Start.Line, sy.Region.End.Line) // take first return } } } pkg.Children.FindNamePrefixScoped(str, &matches) if len(matches) > 0 { for _, sy := range matches { ld.SetFile(sy.Filename, sy.Region.Start.Line, sy.Region.End.Line) // 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/text/parse" "cogentcore.org/core/text/parse/parser" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/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.Start 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 } 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) } 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/text/parse" "cogentcore.org/core/text/parse/parser" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/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" "unicode" "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/base/indent" "cogentcore.org/core/text/parse" "cogentcore.org/core/text/parse/languages" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/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.Dir(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) 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 } 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 textpos.Pos, curLn []rune) (match, newLine bool) { lnLen := len(curLn) if bra == '{' { if pos.Char == lnLen { if lnLen == 0 || unicode.IsSpace(curLn[pos.Char-1]) { newLine = true } match = true } else { match = unicode.IsSpace(curLn[pos.Char]) } } else { match = pos.Char == lnLen || unicode.IsSpace(curLn[pos.Char]) // 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/text/parse" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/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.Since(stt) pr.ParseAll(fs) // prdur := time.Since(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/text/parse" "cogentcore.org/core/text/parse/parser" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/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/text/parse/syms" "cogentcore.org/core/text/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/text/parse" "cogentcore.org/core/text/parse/parser" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/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.ChildASTTry(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 } 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 } 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 } 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/base/errors" "cogentcore.org/core/icons" "cogentcore.org/core/text/csl" "cogentcore.org/core/text/parse" "cogentcore.org/core/text/parse/complete" "cogentcore.org/core/text/parse/languages/bibtex" "cogentcore.org/core/text/textpos" ) // CompleteCite does completion on citation func (ml *MarkdownLang) CompleteCite(fss *parse.FileStates, origStr, str string, pos textpos.Pos) (md complete.Matches) { bfile, has := fss.MetaData("bibfile") if !has { return } if strings.HasSuffix(bfile, ".bib") { 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 } bf, err := ml.CSLs.Open(bfile) if errors.Log(err) != nil { return } md.Seed = str for _, it := range bf.Items.Values { if strings.HasPrefix(it.CitationKey, str) { c := complete.Completion{Text: it.CitationKey, Label: it.CitationKey, 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 textpos.Pos) (ld complete.Lookup) { bfile, has := fss.MetaData("bibfile") if !has { return } if strings.HasSuffix(bfile, ".bib") { 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 } bf, err := ml.CSLs.Open(bfile) if err != nil { return } var items []csl.Item for _, be := range bf.Items.Values { if strings.HasPrefix(be.CitationKey, str) { items = append(items, *be) } } if len(items) > 0 { kl := csl.NewKeyList(items) ld.SetFile(fss.Filename, 0, 0) ld.Text = []byte(kl.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 } if strings.HasSuffix(bfile, ".bib") { _, err := ml.Bibs.Open(bfile) if err != nil { log.Println(err) fss.DeleteMetaData("bibfile") return err } fss.SetMetaData("bibfile", bfile) return nil } _, err := ml.CSLs.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 == "---" || 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 == "---" || lstr == "+++" { return "" } lnln := len(sln) if lnln < trgln { continue } if strings.HasPrefix(lstr, trg) { fnm := lstr[trgln:lnln] if fnm[0] == ':' { return fnm } flds := strings.Fields(fnm) if len(flds) != 2 || flds[0] != "=" { continue } if flds[1][0] == '"' { fnm = flds[1][1 : len(flds[1])-1] 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/text/csl" "cogentcore.org/core/text/parse" "cogentcore.org/core/text/parse/complete" "cogentcore.org/core/text/parse/languages" "cogentcore.org/core/text/parse/languages/bibtex" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/token" ) //go:embed markdown.parse var parserBytes []byte // MarkdownLang implements the Lang interface for the Markdown language type MarkdownLang struct { Pr *parse.Parser // BibBeX bibliography files that have been loaded, // keyed by file path from bibfile metadata stored in filestate. Bibs bibtex.Files // CSL bibliography files that have been loaded, // keyed by file path from bibfile metadata stored in filestate. CSLs csl.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) } // citeKeyStr returns a string with a citation key of the form @[^]Ref // or empty string if not of this form. func citeKeyStr(str string) string { if len(str) < 2 { return "" } if str[0] != '@' { return "" } str = str[1:] if str[0] == '^' { // narrative cite str = str[1:] } return str } func (ml *MarkdownLang) CompleteLine(fss *parse.FileStates, str string, pos textpos.Pos) (md complete.Matches) { origStr := str lfld := lexer.LastField(str) str = citeKeyStr(lexer.InnerBracketScope(lfld, "[", "]")) if str != "" { return ml.CompleteCite(fss, origStr, str, 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 textpos.Pos) (ld complete.Lookup) { origStr := str lfld := lexer.LastField(str) str = citeKeyStr(lexer.InnerBracketScope(lfld, "[", "]")) if str != "" { return ml.LookupCite(fss, origStr, str, 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 textpos.Pos, curLn []rune) (match, newLine bool) { lnLen := len(curLn) match = pos.Char == lnLen || unicode.IsSpace(curLn[pos.Char]) // 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/text/parse" "cogentcore.org/core/text/parse/complete" "cogentcore.org/core/text/parse/languages/bibtex" "cogentcore.org/core/text/textpos" ) // CompleteCite does completion on citation func (tl *TexLang) CompleteCite(fss *parse.FileStates, origStr, str string, pos textpos.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 textpos.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/text/parse" "cogentcore.org/core/text/parse/complete" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/textpos" ) func (tl *TexLang) CompleteLine(fss *parse.FileStates, str string, pos textpos.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 textpos.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/text/parse" "cogentcore.org/core/text/parse/languages" "cogentcore.org/core/text/parse/languages/bibtex" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/textpos" ) //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 textpos.Pos, curLn []rune) (match, newLine bool) { lnLen := len(curLn) match = pos.Char == lnLen || unicode.IsSpace(curLn[pos.Char]) // 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/text/parse/languages" "cogentcore.org/core/text/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/text/textpos" "cogentcore.org/core/text/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 textpos.Pos, maxLns int) (en textpos.Pos, found bool) { en.Line = -1 found = false match, rt := BracePair(r) var left int var right int if rt { right++ } else { left++ } ch := st.Char ln := st.Line 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.Line = l - 1 en.Char = i break } } } } if en.Line >= 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.Line = l + 1 en.Char = i break } } } } if en.Line >= 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/text/textpos" "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 textpos.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.Char { str += "<br>\n\t> " if ssz > e.Pos.Char+30 { str += e.Src[e.Pos.Char : e.Pos.Char+30] } else if ssz > e.Pos.Char { str += e.Src[e.Pos.Char:] } } 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 textpos.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.Line != f.Pos.Line { return e.Pos.Line < f.Pos.Line } if e.Pos.Char != f.Pos.Char { return e.Pos.Char < f.Pos.Char } return e.Msg < f.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 textpos.Pos // initial last.Line is != any legal error line var lastfn string i := 0 for _, e := range *p { if e.Filename != lastfn || e.Pos.Line != last.Line { 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.Line == lstln { continue } str += p[ei].Report(basepath, showSrc, showRule) + "<br>\n" lstln = er.Pos.Line 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/text/textpos" "cogentcore.org/core/text/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 textpos.Pos) bool { if pos.Line < 0 || pos.Line >= fl.NLines() { return false } nt := fl.NTokens(pos.Line) if pos.Char < 0 || pos.Char >= nt { return false } return true } // LexAt returns Lex item at given position, with no checking func (fl *File) LexAt(cp textpos.Pos) *Lex { return &fl.Lexs[cp.Line][cp.Char] } // LexAtSafe returns the Lex item at given position, or last lex item if beyond end func (fl *File) LexAtSafe(cp textpos.Pos) Lex { nln := fl.NLines() if nln == 0 { return Lex{} } if cp.Line >= nln { cp.Line = nln - 1 } sz := len(fl.Lexs[cp.Line]) if sz == 0 { if cp.Line > 0 { cp.Line-- return fl.LexAtSafe(cp) } return Lex{} } if cp.Char < 0 { cp.Char = 0 } if cp.Char >= sz { cp.Char = 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 textpos.Pos) (textpos.Pos, bool) { for pos.Char >= fl.NTokens(pos.Line) { pos.Line++ pos.Char = 0 if pos.Line >= fl.NLines() { pos.Line = 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 textpos.Pos) (textpos.Pos, bool) { pos.Char++ return fl.ValidTokenPos(pos) } // PrevTokenPos returns the previous token position, false if at end of tokens func (fl *File) PrevTokenPos(pos textpos.Pos) (textpos.Pos, bool) { pos.Char-- if pos.Char < 0 { pos.Line-- if pos.Line < 0 { return pos, false } for fl.NTokens(pos.Line) == 0 { pos.Line-- if pos.Line < 0 { pos.Line = 0 pos.Char = 0 return pos, false } } pos.Char = fl.NTokens(pos.Line) - 1 } return pos, true } // Token gets lex token at given Pos (Ch = token index) func (fl *File) Token(pos textpos.Pos) token.KeyToken { return fl.Lexs[pos.Line][pos.Char].Token } // PrevDepth returns the depth of the token immediately prior to given line func (fl *File) PrevDepth(ln int) int { pos := textpos.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 textpos.Region) TokenMap { m := make(TokenMap) cp, ok := fl.ValidTokenPos(reg.Start) for ok && cp.IsLess(reg.End) { 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 textpos.Pos) []rune { if !fl.IsLexPosValid(pos) { return nil } lx := fl.Lexs[pos.Line][pos.Char] return fl.Lines[pos.Line][lx.Start:lx.End] } // TokenSrcPos returns source reg associated with lex token at given token position func (fl *File) TokenSrcPos(pos textpos.Pos) textpos.Region { if !fl.IsLexPosValid(pos) { return textpos.Region{} } lx := fl.Lexs[pos.Line][pos.Char] return textpos.Region{Start: textpos.Pos{pos.Line, lx.Start}, End: textpos.Pos{pos.Line, lx.End}} } // TokenSrcReg translates a region of tokens into a region of source func (fl *File) TokenSrcReg(reg textpos.Region) textpos.Region { if !fl.IsLexPosValid(reg.Start) || reg.IsNil() { return textpos.Region{} } st := fl.Lexs[reg.Start.Line][reg.Start.Char].Start ep, _ := fl.PrevTokenPos(reg.End) // ed is exclusive -- go to prev ed := fl.Lexs[ep.Line][ep.Char].End return textpos.Region{Start: textpos.Pos{reg.Start.Line, st}, End: textpos.Pos{ep.Line, ed}} } // RegSrc returns the source (as a string) for given region func (fl *File) RegSrc(reg textpos.Region) string { if reg.End.Line == reg.Start.Line { if reg.End.Char > reg.Start.Char { return string(fl.Lines[reg.End.Line][reg.Start.Char:reg.End.Char]) } return "" } src := string(fl.Lines[reg.Start.Line][reg.Start.Char:]) nln := reg.End.Line - reg.Start.Line if nln > 10 { src += "|>" + string(fl.Lines[reg.Start.Line+1]) + "..." src += "|>" + string(fl.Lines[reg.End.Line-1]) return src } for ln := reg.Start.Line + 1; ln < reg.End.Line; ln++ { src += "|>" + string(fl.Lines[ln]) } src += "|>" + string(fl.Lines[reg.End.Line][:reg.End.Char]) return src } // TokenRegSrc returns the source code associated with the given token region func (fl *File) TokenRegSrc(reg textpos.Region) string { if !fl.IsLexPosValid(reg.Start) { 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 textpos.Pos) textpos.Pos { np := textpos.Pos{cp.Line, cp.Char + 1} elx := fl.LexAt(cp) depth := elx.Token.Depth fl.Lexs[cp.Line].Insert(np.Char, Lex{Token: token.KeyToken{Token: token.EOS, Depth: depth}, Start: elx.End, End: elx.End}) fl.EosPos[np.Line] = append(fl.EosPos[np.Line], np.Char) return np } // ReplaceEos replaces given token with an EOS func (fl *File) ReplaceEos(cp textpos.Pos) { clex := fl.LexAt(cp) clex.Token.Token = token.EOS fl.EosPos[cp.Line] = append(fl.EosPos[cp.Line], cp.Char) } // 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 := textpos.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 textpos.Pos, depth int) (textpos.Pos, bool) { // prf := profile.Start("NextEos") // defer prf.End() ep := stpos nlines := fl.NLines() if stpos.Line >= nlines { return ep, false } eps := fl.EosPos[stpos.Line] for i := range eps { if eps[i] < stpos.Char { continue } ep.Char = eps[i] lx := fl.LexAt(ep) if lx.Token.Depth == depth { return ep, true } } for ep.Line = stpos.Line + 1; ep.Line < nlines; ep.Line++ { eps := fl.EosPos[ep.Line] sz := len(eps) if sz == 0 { continue } for i := 0; i < sz; i++ { ep.Char = 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 textpos.Pos) (textpos.Pos, bool) { ep := stpos nlines := fl.NLines() if stpos.Line >= nlines { return ep, false } eps := fl.EosPos[stpos.Line] if np := eps.FindGtEq(stpos.Char); np >= 0 { ep.Char = np return ep, true } ep.Char = 0 for ep.Line = stpos.Line + 1; ep.Line < nlines; ep.Line++ { sz := len(fl.EosPos[ep.Line]) 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/text/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 } 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/text/textpos" "cogentcore.org/core/text/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 Start int // end rune index within original source line for this token (exclusive -- ends one before this) End 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, Start: st, End: 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.Start:lx.End] } // 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.Start, lx.End, lx.Token.String()) } // ContainsPos returns true if the Lex element contains given character position func (lx *Lex) ContainsPos(pos int) bool { return pos >= lx.Start && pos < lx.End } // OverlapsReg returns true if the two regions overlap func (lx *Lex) OverlapsReg(or Lex) bool { // start overlaps if (lx.Start >= or.Start && lx.Start < or.End) || (or.Start >= lx.Start && or.Start < lx.End) { return true } // end overlaps return (lx.End > or.Start && lx.End <= or.End) || (or.End > lx.Start && or.End <= lx.End) } // Region returns the region for this lexical element, at given line func (lx *Lex) Region(ln int) textpos.Region { return textpos.Region{Start: textpos.Pos{Line: ln, Char: lx.Start}, End: textpos.Pos{Line: ln, Char: lx.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 lexer import ( "slices" "sort" "unicode" "cogentcore.org/core/text/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.Start < lx.Start { continue } if t.Start == lx.Start && t.End >= lx.End { 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].Start < (*ll)[j].Start || ((*ll)[i].Start == (*ll)[j].Start && (*ll)[i].End > (*ll)[j].End) }) } // 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.Start:t.End]) } 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.Start; i < t.End; 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.Start = i } } else { if cspc { cur.End = i ln.Add(cur) } } pspc = cspc } if !pspc { cur.End = 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 ( "strings" "unicode" "cogentcore.org/core/text/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. This is for spell checking: also excludes // any _ values. func FirstWordApostrophe(str string) string { rstr := "" for _, s := range str { if !(IsLetterNoUnderbar(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.Start > 1 { _, lxidx := line.AtPos(lx.Start - 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) } // 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 IsLetterNoUnderbar(ch rune) bool { return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || (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/text/textpos" "cogentcore.org/core/text/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 textpos.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 = textpos.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.Line++ ts.Pos.Char = 0 } // Error adds an passtwo error at current position func (ts *TwoState) Error(msg string) { ppos := ts.Pos ppos.Char-- clex := ts.Src.LexAtSafe(ppos) ts.Errs.Add(textpos.Pos{ts.Pos.Line, clex.Start}, ts.Src.Filename, "PassTwo: "+msg, ts.Src.SrcLine(ts.Pos.Line), 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.Line < nlines { sz := len(ts.Src.Lexs[ts.Pos.Line]) 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.Char++ if ts.Pos.Char >= 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, textpos.PosZero, nlines) } // Perform EOS detection at given starting position, for given number of lines func (pt *PassTwo) EosDetectPos(ts *TwoState, pos textpos.Pos, nln int) { ts.Pos = pos nlines := ts.Src.NLines() ok := false for lc := 0; ts.Pos.Line < nlines && lc < nln; lc++ { sz := len(ts.Src.Lexs[ts.Pos.Line]) if sz == 0 { ts.NextLine() continue } if pt.Semi { for ts.Pos.Char = 0; ts.Pos.Char < sz; ts.Pos.Char++ { 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(textpos.Pos{ts.Pos.Line, ci}) if lx.Token.Token == token.PunctGpRBrace { if ci == 0 { ip := textpos.Pos{ts.Pos.Line, 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 := textpos.Pos{ts.Pos.Line, ci - 1} ilx := ts.Src.LexAt(ip) if ilx.Token.Token != token.PunctGpLBrace { ts.Src.InsertEos(ip) ci++ sz++ } } if ci == sz-1 { ip := textpos.Pos{ts.Pos.Line, ci} ts.Src.InsertEos(ip) sz++ skip = true } } } if skip { ts.NextLine() continue } } ep := textpos.Pos{ts.Pos.Line, sz - 1} // end of line token elx := ts.Src.LexAt(ep) if pt.Eol { sp := textpos.Pos{ts.Pos.Line, 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 ( "cogentcore.org/core/text/token" ) // 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/text/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.RuneAt(lr.Offset) if !ok { return false } if IsLetter(rn) { return true } return false case Digit: rn, ok := ls.RuneAt(lr.Offset) if !ok { return false } if IsDigit(rn) { return true } return false case WhiteSpace: rn, ok := ls.RuneAt(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.RuneAt(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/text/textpos" "cogentcore.org/core/text/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) Line int // the current rune read by NextRune Rune 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.Line = 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.Line, 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(textpos.Pos{ls.Line, 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) RuneAt(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.Rune = ls.Src[ls.Pos] return true } // CurRune reads the current rune into Ch and returns false if at end of line func (ls *State) CurRuneAt() bool { sz := len(ls.Src) if ls.Pos >= sz { ls.Pos = sz return false } ls.Rune = 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.End == st { lst.End = 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.Rune == match { depth++ continue } else if ls.Rune == 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.Rune == '|' { 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.CurRuneAt() if ls.Rune == '0' { // int or float offs := ls.Pos ls.NextRune() if ls.Rune == 'x' || ls.Rune == '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.Rune == '8' || ls.Rune == '9' { // illegal octal int or float seenDecimalDigit = true ls.ScanMantissa(10) } if ls.Rune == '.' || ls.Rune == 'e' || ls.Rune == 'E' || ls.Rune == '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.Rune == '.' { tok = token.LitNumFloat ls.NextRune() ls.ScanMantissa(10) } if ls.Rune == 'e' || ls.Rune == 'E' { tok = token.LitNumFloat ls.NextRune() if ls.Rune == '-' || ls.Rune == '+' { ls.NextRune() } if DigitValue(ls.Rune) < 10 { ls.ScanMantissa(10) } else { ls.Error(offs, "illegal floating-point exponent", nil) } } if ls.Rune == '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.Rune) < base { if !ls.NextRune() { break } } } func (ls *State) ReadQuoted() { delim, _ := ls.RuneAt(0) offs := ls.Pos ls.NextRune() for { ch := ls.Rune 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.Rune { 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.Rune < 0 { msg = "escape sequence not terminated" } ls.Error(offs, msg, nil) return false } var x uint32 for n > 0 { d := uint32(DigitValue(ls.Rune)) if d >= base { msg := fmt.Sprintf("illegal character %#U in escape sequence", ls.Rune) if ls.Rune < 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/text/token" "cogentcore.org/core/tree" "cogentcore.org/core/types" ) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/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/text/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/text/parse/lexer" "cogentcore.org/core/text/parse/parser" "cogentcore.org/core/text/textpos" ) // 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.Line >= fs.Src.NLines() { return nil } for { if fs.LexState.AtEol() { fs.Src.SetLine(fs.LexState.Line, fs.LexState.Lex, fs.LexState.Comments, fs.LexState.Stack) fs.LexState.Line++ if fs.LexState.Line >= fs.Src.NLines() { return nil } fs.LexState.SetLine(fs.Src.Lines[fs.LexState.Line]) } 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.Line >= fs.Src.NLines() { return nil } var mrule *lexer.Rule for { if fs.LexState.AtEol() { fs.Src.SetLine(fs.LexState.Line, fs.LexState.Lex, fs.LexState.Comments, fs.LexState.Stack) fs.LexState.Line++ if fs.LexState.Line >= fs.Src.NLines() { return nil } fs.LexState.SetLine(fs.Src.Lines[fs.LexState.Line]) 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, textpos.Pos{Line: 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/text/parse/lexer" "cogentcore.org/core/text/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) } 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/text/parse/lexer" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/textpos" "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 textpos.Region `set:"-"` // region in source file corresponding to this AST node SrcReg textpos.Region `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) } // ChildASTTry returns the Child at given index as an AST, // nil if not in range. func (ast *AST) ChildASTTry(idx int) *AST { if ast == nil { return nil } if idx < 0 || idx >= len(ast.Children) { return nil } 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 textpos.Region, 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 textpos.Pos, src *lexer.File) { ast.TokReg.End = 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/text/parse/lexer" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/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 []textpos.Region // StartEnd returns the first and last non-zero positions in the Matches list as a region func (mm Matches) StartEnd() textpos.Region { reg := textpos.RegionZero for _, mp := range mm { if mp.Start != textpos.PosZero { if reg.Start == textpos.PosZero { reg.Start = mp.Start } reg.End = mp.End } } 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) textpos.Region { reg := mm.StartEnd() reg.End, _ = ps.Src.NextTokenPos(reg.End) 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(textpos.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(textpos.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(textpos.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(textpos.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(textpos.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(textpos.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(textpos.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(textpos.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(textpos.PosZero, "Validate: rule has no rules and no children", pr) valid = false } if !pr.tokenMatchGroup && len(pr.Rules) > 0 && pr.HasChildren() { ps.Error(textpos.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(textpos.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(textpos.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(textpos.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(textpos.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 := textpos.Region{Start: ps.Pos} if ps.AST.HasChildren() { parAST = ps.AST.ChildAST(0) } else { parAST = NewAST(ps.AST) parAST.SetName(kpr.Name) ok := false scope.Start, ok = ps.Src.ValidTokenPos(scope.Start) if !ok { ps.GotoEof() return nil } ps.Pos = scope.Start } 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 textpos.Region, optMap lexer.TokenMap, depth int) *Rule { if pr.Off { return nil } if depth >= DepthLimit { ps.Error(scope.Start, "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.Start, 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 textpos.Region, 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 == textpos.RegionZero { ps.Error(scope.Start, "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 textpos.Region) (textpos.Region, 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.Start) ep, ok := ps.Src.NextEos(creg.Start, stlx.Token.Depth) if !ok { // ps.Error(creg.Start, "could not find EOS at target nesting depth -- parens / bracket / brace mismatch?", pr) return nscope, false } if scope.End != textpos.PosZero && lr.Opt && scope.End.IsLess(ep) { // optional tokens can't take us out of scope return scope, true } if ei == lr.StInc-1 { nscope.End = ep if ps.Trace.On { ps.Trace.Out(ps, pr, SubMatch, nscope.Start, 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.Start, 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 textpos.Region, depth int, optMap lexer.TokenMap) (bool, textpos.Region, Matches) { if pr.Off { return false, scope, nil } if depth > DepthLimit { ps.Error(scope.Start, "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.Start, 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.Start, 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 textpos.Region, depth int, optMap lexer.TokenMap) (bool, Matches) { nr := len(pr.Rules) var mpos Matches scstlx := ps.Src.LexAt(scope.Start) // 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] = textpos.Region{Start: scope.End, End: scope.End} } 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 != textpos.PosZero { tlx := ps.Src.LexAt(tpos) ps.Trace.Out(ps, pr, NoMatch, creg.Start, creg, parAST, fmt.Sprintf("%v token: %v, was: %v", ri, kt.String(), tlx.String())) } else { ps.Trace.Out(ps, pr, NoMatch, creg.Start, 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] = textpos.Region{Start: tpos, End: tpos} if ps.Trace.On { ps.Trace.Out(ps, pr, SubMatch, creg.Start, 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 *textpos.Region, mpos Matches, parAST *AST, scope textpos.Region, depth int, optMap lexer.TokenMap) (bool, textpos.Pos) { nr := len(pr.Rules) ok := false matchst := false // match start of creg matched := false // match end of creg var tpos textpos.Pos if ri == 0 { matchst = true } else if mpos != nil { lpos := mpos[ri-1].End if lpos != textpos.PosZero { // previous has matched matchst = true } else if ri < nr-1 && rr.FromNext { lpos := mpos[ri+1].Start if lpos != textpos.PosZero { // previous has matched creg.End, _ = ps.Src.PrevTokenPos(lpos) matched = true } } } for stinc := 0; stinc < rr.StInc; stinc++ { creg.Start, _ = ps.Src.NextTokenPos(creg.Start) } if ri == nr-1 && rr.Token.Token == token.EOS { return true, scope.End } if creg.IsNil() && !matched { return false, tpos } if matchst { // start token must be right here if !ps.MatchToken(kt, creg.Start) { return false, creg.Start } tpos = creg.Start } else if matched { if !ps.MatchToken(kt, creg.End) { return false, creg.End } tpos = creg.End } 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.Start, _ = 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 textpos.Region, depth int, optMap lexer.TokenMap) (bool, Matches) { nr := len(pr.Rules) var mpos Matches scstlx := ps.Src.LexAt(scope.Start) // 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] = textpos.Region{Start: scope.End, End: scope.End} } 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 != textpos.PosZero { tlx := ps.Src.LexAt(tpos) ps.Trace.Out(ps, pr, NoMatch, creg.Start, creg, parAST, fmt.Sprintf("%v token: %v, was: %v", ri, kt.String(), tlx.String())) } else { ps.Trace.Out(ps, pr, NoMatch, creg.Start, 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] = textpos.Region{Start: tpos, End: tpos} if ps.Trace.On { ps.Trace.Out(ps, pr, SubMatch, creg.Start, creg, parAST, fmt.Sprintf("%v token: %v", ri, kt.String())) } continue } ////////////////////////////////////////////// // Sub-Rule if creg.IsNil() { ps.Trace.Out(ps, pr, NoMatch, creg.Start, 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.Start) // scope starting lex cp, _ := ps.Src.NextTokenPos(creg.Start) stdp := stlx.Token.Depth for cp.IsLess(creg.End) { lx := ps.Src.LexAt(cp) if lx.Token.Depth < stdp { creg.End = cp break } cp, _ = ps.Src.NextTokenPos(cp) } if ps.Trace.On { ps.Trace.Out(ps, pr, SubMatch, creg.Start, 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.Start, creg, parAST, fmt.Sprintf("%v sub-rule: %v", ri, rr.Rule.Name)) } return false, nil } creg.End = scope.End // 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.End) 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.Start, 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.Start = lmnpos if ps.Trace.On { msreg := mreg msreg.End = lmnpos ps.Trace.Out(ps, pr, SubMatch, mreg.Start, 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 textpos.Region, 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.Start, 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.Start, 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.Start, 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 textpos.Region, depth int, optMap lexer.TokenMap) (bool, textpos.Region, 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.Start) 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.Start, 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.Start, 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 textpos.Region, ktpos textpos.Region, depth int, optMap lexer.TokenMap) bool { nf := len(pr.ExclFwd) nr := len(pr.ExclRev) scstlx := ps.Src.LexAt(scope.Start) // scope starting lex scstDepth := scstlx.Token.Depth if nf > 0 { cp, ok := ps.Src.NextTokenPos(ktpos.Start) 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.Start = 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.End == cp || scope.End.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.Start) 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.End = 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.Start) { 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 textpos.Region, 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.Start, scope, trcAST, fmt.Sprintf("running with new ast: %v", trcAST.Path())) } } else { if ps.Trace.On { ps.Trace.Out(ps, pr, Run, scope.Start, 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].Start 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.Start = ps.Pos creg.End = scope.End if !pr.noTokens { for mi := ri + 1; mi < nr; mi++ { if mpos[mi].Start != textpos.PosZero { creg.End = mpos[mi].Start // 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.Start, scope, trcAST, fmt.Sprintf("%v: opt token: %v no more src", ri, rr.Token)) } continue } stlx := ps.Src.LexAt(creg.Start) 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.Start, 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.Start, scope, trcAST, fmt.Sprintf("%v: opt rule: %v no more src", ri, rr.Rule.Name)) } continue } ps.Error(creg.Start, 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.Start, 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.Start, fmt.Sprintf("required element: %v did not match input", rr.Rule.Name), pr) valid = false break } if ps.Trace.On { ps.Trace.Out(ps, pr, Run, creg.Start, 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 textpos.Region, 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].Start aftMpos, ok := ps.Src.NextTokenPos(tokpos) if !ok { ps.Error(tokpos, "premature end of input", pr) return false } epos := scope.End for i := nr - 1; i >= 0; i-- { rr := &pr.Rules[i] if i > 1 { creg.Start = aftMpos // end expr is in region from key token to end of scope ps.Pos = creg.Start // only works for a single rule after key token -- sub-rules not necc reverse creg.End = scope.End } 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.Start = scope.Start ps.Pos = creg.Start creg.End = tokpos } if rr.IsRule() { // non-key tokens ignored if creg.IsNil() { // no tokens left.. ps.Error(creg.Start, 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.Start, 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.Start, 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, textpos.RegionZero, 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.Start) 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.Start for cp.IsLess(ast.TokReg.End) { 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.Start for cp.IsLess(nast.TokReg.End) { 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.Start, 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.Start, 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.Start, 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) // textpos.RegionZero) // 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.Start, 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.Start, 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.Start, 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.Start, 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.Start, 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.Start, 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.Start, 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.Start, 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.Start, 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/text/parse/lexer" "cogentcore.org/core/text/parse/syms" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/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 textpos.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(textpos.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(textpos.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 textpos.Pos, msg string, rule *Rule) { if pos != textpos.PosZero { pos = ps.Src.TokenSrcPos(pos).Start } e := ps.Errs.Add(pos, ps.Src.Filename, msg, ps.Src.SrcLine(pos.Line), 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.Line >= 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.Line == ps.Src.NLines()-1 } // GotoEof sets current position at EOF func (ps *State) GotoEof() { ps.Pos.Line = ps.Src.NLines() ps.Pos.Char = 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.Char = ps.Src.NTokens(ep.Line) if ep.Char == sp.Char+1 { // only one nep, ok := ps.Src.ValidTokenPos(ep) if ok { ep = nep ep.Char = ps.Src.NTokens(ep.Line) } } reg := textpos.Region{Start: sp, End: 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 textpos.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.Start start at the start of the search. // All positions in token indexes. func (ps *State) FindToken(tkey token.KeyToken, reg textpos.Region) (textpos.Pos, bool) { // prf := profile.Start("FindToken") // defer prf.End() cp, ok := ps.Src.ValidTokenPos(reg.Start) if !ok { return cp, false } tok := tkey.Token isCat := tok.Cat() == tok isSubCat := tok.SubCat() == tok for cp.IsLess(reg.End) { 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 textpos.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.End-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 textpos.Region) (textpos.Pos, bool) { // prf := profile.Start("FindTokenReverse") // defer prf.End() cp, ok := ps.Src.PrevTokenPos(reg.End) if !ok { return cp, false } tok := tkey.Token isCat := tok.Cat() == tok isSubCat := tok.SubCat() == tok isAmbigUnary := tok.IsAmbigUnaryOp() for reg.Start.IsLess(cp) || cp == reg.Start { 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 textpos.Region) *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 textpos.Region // 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 textpos.Region, 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 textpos.Region) (*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 textpos.Region, regs Matches) { rs := &ps.Matches[scope.Start.Line][scope.Start.Char] 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 textpos.Region) (*MatchState, bool) { rs := &ps.Matches[scope.Start.Line][scope.Start.Char] 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(textpos.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 textpos.Region 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 textpos.Region, pr *Rule) { sr := ScopeRule{scope, pr} rs[sr] = struct{}{} } // Has checks if scope rule set has given scope, rule func (rs ScopeRuleSet) Has(scope textpos.Region, 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 textpos.Region, pr *Rule) { ps.NonMatches.Add(scope, pr) } // IsNonMatch looks for rule in nonmatch list at given scope func (ps *State) IsNonMatch(scope textpos.Region, 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/text/textpos" ) // 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 [textcore.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 textpos.Pos, scope textpos.Region, 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/text/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/text/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/text/parse/complete" "cogentcore.org/core/text/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/text/textpos" "cogentcore.org/core/text/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 textpos.Region // region that should be selected when activated, etc SelectReg textpos.Region // 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 textpos.Region) *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 == textpos.RegionZero } // 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/text/parse/lexer" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/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 textpos.Region) *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 textpos.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.End.Line += 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/text/textpos" "cogentcore.org/core/text/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 textpos.Region) *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/text/textpos" "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 textpos.Region // 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) } 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 rich import ( "cogentcore.org/core/enums" ) var _FamilyValues = []Family{0, 1, 2, 3, 4, 5, 6, 7, 8} // FamilyN is the highest valid value for type Family, plus one. const FamilyN Family = 9 var _FamilyValueMap = map[string]Family{`sans-serif`: 0, `serif`: 1, `monospace`: 2, `cursive`: 3, `fantasy`: 4, `math`: 5, `emoji`: 6, `fangsong`: 7, `custom`: 8} var _FamilyDescMap = map[Family]string{0: `SansSerif is a font without serifs, where glyphs have plain stroke endings, without ornamentation. Example sans-serif fonts include Arial, Helvetica, Open Sans, Fira Sans, Lucida Sans, Lucida Sans Unicode, Trebuchet MS, Liberation Sans, and Nimbus Sans L.`, 1: `Serif is a small line or stroke attached to the end of a larger stroke in a letter. In serif fonts, glyphs have finishing strokes, flared or tapering ends. Examples include Times New Roman, Lucida Bright, Lucida Fax, Palatino, Palatino Linotype, Palladio, and URW Palladio.`, 2: `Monospace fonts have all glyphs with he same fixed width. Example monospace fonts include Fira Mono, DejaVu Sans Mono, Menlo, Consolas, Liberation Mono, Monaco, and Lucida Console.`, 3: `Cursive glyphs generally have either joining strokes or other cursive characteristics beyond those of italic typefaces. The glyphs are partially or completely connected, and the result looks more like handwritten pen or brush writing than printed letter work. Example cursive fonts include Brush Script MT, Brush Script Std, Lucida Calligraphy, Lucida Handwriting, and Apple Chancery.`, 4: `Fantasy fonts are primarily decorative fonts that contain playful representations of characters. Example fantasy fonts include Papyrus, Herculanum, Party LET, Curlz MT, and Harrington.`, 5: `Math fonts are for displaying mathematical expressions, for example superscript and subscript, brackets that cross several lines, nesting expressions, and double-struck glyphs with distinct meanings.`, 6: `Emoji fonts are specifically designed to render emoji.`, 7: `Fangsong are a particular style of Chinese characters that are between serif-style Song and cursive-style Kai forms. This style is often used for government documents.`, 8: `Custom is a custom font name that is specified in the [text.Style] CustomFont name.`} var _FamilyMap = map[Family]string{0: `sans-serif`, 1: `serif`, 2: `monospace`, 3: `cursive`, 4: `fantasy`, 5: `math`, 6: `emoji`, 7: `fangsong`, 8: `custom`} // String returns the string representation of this Family value. func (i Family) String() string { return enums.String(i, _FamilyMap) } // SetString sets the Family value from its string representation, // and returns an error if the string is invalid. func (i *Family) SetString(s string) error { return enums.SetString(i, s, _FamilyValueMap, "Family") } // Int64 returns the Family value as an int64. func (i Family) Int64() int64 { return int64(i) } // SetInt64 sets the Family value from an int64. func (i *Family) SetInt64(in int64) { *i = Family(in) } // Desc returns the description of the Family value. func (i Family) Desc() string { return enums.Desc(i, _FamilyDescMap) } // FamilyValues returns all possible values for the type Family. func FamilyValues() []Family { return _FamilyValues } // Values returns all possible values for the type Family. func (i Family) Values() []enums.Enum { return enums.Values(_FamilyValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Family) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Family) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Family") } var _SlantsValues = []Slants{0, 1} // SlantsN is the highest valid value for type Slants, plus one. const SlantsN Slants = 2 var _SlantsValueMap = map[string]Slants{`normal`: 0, `italic`: 1} var _SlantsDescMap = map[Slants]string{0: `A face that is neither italic not obliqued.`, 1: `A form that is generally cursive in nature or slanted. This groups what is usually called Italic or Oblique.`} var _SlantsMap = map[Slants]string{0: `normal`, 1: `italic`} // String returns the string representation of this Slants value. func (i Slants) String() string { return enums.String(i, _SlantsMap) } // SetString sets the Slants value from its string representation, // and returns an error if the string is invalid. func (i *Slants) SetString(s string) error { return enums.SetString(i, s, _SlantsValueMap, "Slants") } // Int64 returns the Slants value as an int64. func (i Slants) Int64() int64 { return int64(i) } // SetInt64 sets the Slants value from an int64. func (i *Slants) SetInt64(in int64) { *i = Slants(in) } // Desc returns the description of the Slants value. func (i Slants) Desc() string { return enums.Desc(i, _SlantsDescMap) } // SlantsValues returns all possible values for the type Slants. func SlantsValues() []Slants { return _SlantsValues } // Values returns all possible values for the type Slants. func (i Slants) Values() []enums.Enum { return enums.Values(_SlantsValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Slants) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Slants) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Slants") } var _WeightsValues = []Weights{0, 1, 2, 3, 4, 5, 6, 7, 8} // WeightsN is the highest valid value for type Weights, plus one. const WeightsN Weights = 9 var _WeightsValueMap = map[string]Weights{`thin`: 0, `extra-light`: 1, `light`: 2, `normal`: 3, `medium`: 4, `semibold`: 5, `bold`: 6, `extra-bold`: 7, `black`: 8} var _WeightsDescMap = map[Weights]string{0: `Thin weight (100), the thinnest value.`, 1: `Extra light weight (200).`, 2: `Light weight (300).`, 3: `Normal (400).`, 4: `Medium weight (500, higher than normal).`, 5: `Semibold weight (600).`, 6: `Bold weight (700).`, 7: `Extra-bold weight (800).`, 8: `Black weight (900), the thickest value.`} var _WeightsMap = map[Weights]string{0: `thin`, 1: `extra-light`, 2: `light`, 3: `normal`, 4: `medium`, 5: `semibold`, 6: `bold`, 7: `extra-bold`, 8: `black`} // String returns the string representation of this Weights value. func (i Weights) String() string { return enums.String(i, _WeightsMap) } // SetString sets the Weights value from its string representation, // and returns an error if the string is invalid. func (i *Weights) SetString(s string) error { return enums.SetString(i, s, _WeightsValueMap, "Weights") } // Int64 returns the Weights value as an int64. func (i Weights) Int64() int64 { return int64(i) } // SetInt64 sets the Weights value from an int64. func (i *Weights) SetInt64(in int64) { *i = Weights(in) } // Desc returns the description of the Weights value. func (i Weights) Desc() string { return enums.Desc(i, _WeightsDescMap) } // WeightsValues returns all possible values for the type Weights. func WeightsValues() []Weights { return _WeightsValues } // Values returns all possible values for the type Weights. func (i Weights) Values() []enums.Enum { return enums.Values(_WeightsValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Weights) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Weights) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Weights") } var _StretchValues = []Stretch{0, 1, 2, 3, 4, 5, 6, 7, 8} // StretchN is the highest valid value for type Stretch, plus one. const StretchN Stretch = 9 var _StretchValueMap = map[string]Stretch{`ultra-condensed`: 0, `extra-condensed`: 1, `condensed`: 2, `semi-condensed`: 3, `normal`: 4, `semi-expanded`: 5, `expanded`: 6, `extra-expanded`: 7, `ultra-expanded`: 8} var _StretchDescMap = map[Stretch]string{0: `Ultra-condensed width (50%), the narrowest possible.`, 1: `Extra-condensed width (62.5%).`, 2: `Condensed width (75%).`, 3: `Semi-condensed width (87.5%).`, 4: `Normal width (100%).`, 5: `Semi-expanded width (112.5%).`, 6: `Expanded width (125%).`, 7: `Extra-expanded width (150%).`, 8: `Ultra-expanded width (200%), the widest possible.`} var _StretchMap = map[Stretch]string{0: `ultra-condensed`, 1: `extra-condensed`, 2: `condensed`, 3: `semi-condensed`, 4: `normal`, 5: `semi-expanded`, 6: `expanded`, 7: `extra-expanded`, 8: `ultra-expanded`} // String returns the string representation of this Stretch value. func (i Stretch) String() string { return enums.String(i, _StretchMap) } // SetString sets the Stretch value from its string representation, // and returns an error if the string is invalid. func (i *Stretch) SetString(s string) error { return enums.SetString(i, s, _StretchValueMap, "Stretch") } // Int64 returns the Stretch value as an int64. func (i Stretch) Int64() int64 { return int64(i) } // SetInt64 sets the Stretch value from an int64. func (i *Stretch) SetInt64(in int64) { *i = Stretch(in) } // Desc returns the description of the Stretch value. func (i Stretch) Desc() string { return enums.Desc(i, _StretchDescMap) } // StretchValues returns all possible values for the type Stretch. func StretchValues() []Stretch { return _StretchValues } // Values returns all possible values for the type Stretch. func (i Stretch) Values() []enums.Enum { return enums.Values(_StretchValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Stretch) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Stretch) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Stretch") } var _DecorationsValues = []Decorations{0, 1, 2, 3, 4, 5, 6, 7} // DecorationsN is the highest valid value for type Decorations, plus one. const DecorationsN Decorations = 8 var _DecorationsValueMap = map[string]Decorations{`underline`: 0, `overline`: 1, `line-through`: 2, `dotted-underline`: 3, `paragraph-start`: 4, `fill-color`: 5, `stroke-color`: 6, `background`: 7} var _DecorationsDescMap = map[Decorations]string{0: `Underline indicates to place a line below text.`, 1: `Overline indicates to place a line above text.`, 2: `LineThrough indicates to place a line through text.`, 3: `DottedUnderline is used for abbr tag.`, 4: `ParagraphStart indicates that this text is the start of a paragraph, and therefore may be indented according to [text.Style] settings.`, 5: `FillColor means that the fill color of the glyph is set. The standard font rendering uses this fill color (compare to StrokeColor).`, 6: `StrokeColor means that the stroke color of the glyph is set. This is normally not rendered: it looks like an outline of the glyph at larger font sizes, and will make smaller font sizes look significantly thicker.`, 7: `Background means that the background region behind the text is colored. The background is not normally colored so it renders over any background.`} var _DecorationsMap = map[Decorations]string{0: `underline`, 1: `overline`, 2: `line-through`, 3: `dotted-underline`, 4: `paragraph-start`, 5: `fill-color`, 6: `stroke-color`, 7: `background`} // String returns the string representation of this Decorations value. func (i Decorations) String() string { return enums.BitFlagString(i, _DecorationsValues) } // BitIndexString returns the string representation of this Decorations value // if it is a bit index value (typically an enum constant), and // not an actual bit flag value. func (i Decorations) BitIndexString() string { return enums.String(i, _DecorationsMap) } // SetString sets the Decorations value from its string representation, // and returns an error if the string is invalid. func (i *Decorations) SetString(s string) error { *i = 0; return i.SetStringOr(s) } // SetStringOr sets the Decorations value from its string representation // while preserving any bit flags already set, and returns an // error if the string is invalid. func (i *Decorations) SetStringOr(s string) error { return enums.SetStringOr(i, s, _DecorationsValueMap, "Decorations") } // Int64 returns the Decorations value as an int64. func (i Decorations) Int64() int64 { return int64(i) } // SetInt64 sets the Decorations value from an int64. func (i *Decorations) SetInt64(in int64) { *i = Decorations(in) } // Desc returns the description of the Decorations value. func (i Decorations) Desc() string { return enums.Desc(i, _DecorationsDescMap) } // DecorationsValues returns all possible values for the type Decorations. func DecorationsValues() []Decorations { return _DecorationsValues } // Values returns all possible values for the type Decorations. func (i Decorations) Values() []enums.Enum { return enums.Values(_DecorationsValues) } // HasFlag returns whether these bit flags have the given bit flag set. func (i *Decorations) 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 *Decorations) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Decorations) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Decorations) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Decorations") } var _SpecialsValues = []Specials{0, 1, 2, 3, 4, 5, 6, 7} // SpecialsN is the highest valid value for type Specials, plus one. const SpecialsN Specials = 8 var _SpecialsValueMap = map[string]Specials{`nothing`: 0, `super`: 1, `sub`: 2, `link`: 3, `math-inline`: 4, `math-display`: 5, `quote`: 6, `end`: 7} var _SpecialsDescMap = map[Specials]string{0: `Nothing special.`, 1: `Super starts super-scripted text.`, 2: `Sub starts sub-scripted text.`, 3: `Link starts a hyperlink, which is in the URL field of the style, and encoded in the runes after the style runes. It also identifies this span for functional interactions such as hovering and clicking. It does not specify the styling, which therefore must be set in addition.`, 4: `MathInline starts a TeX formatted math sequence, styled for inclusion inline with other text.`, 5: `MathDisplay starts a TeX formatted math sequence, styled as a larger standalone display.`, 6: `Quote starts an indented paragraph-level quote.`, 7: `End must be added to terminate the last Special started: use [Text.AddEnd]. The renderer maintains a stack of special elements.`} var _SpecialsMap = map[Specials]string{0: `nothing`, 1: `super`, 2: `sub`, 3: `link`, 4: `math-inline`, 5: `math-display`, 6: `quote`, 7: `end`} // String returns the string representation of this Specials value. func (i Specials) String() string { return enums.String(i, _SpecialsMap) } // SetString sets the Specials value from its string representation, // and returns an error if the string is invalid. func (i *Specials) SetString(s string) error { return enums.SetString(i, s, _SpecialsValueMap, "Specials") } // Int64 returns the Specials value as an int64. func (i Specials) Int64() int64 { return int64(i) } // SetInt64 sets the Specials value from an int64. func (i *Specials) SetInt64(in int64) { *i = Specials(in) } // Desc returns the description of the Specials value. func (i Specials) Desc() string { return enums.Desc(i, _SpecialsDescMap) } // SpecialsValues returns all possible values for the type Specials. func SpecialsValues() []Specials { return _SpecialsValues } // Values returns all possible values for the type Specials. func (i Specials) Values() []enums.Enum { return enums.Values(_SpecialsValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i Specials) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Specials) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Specials") } var _DirectionsValues = []Directions{0, 1, 2, 3, 4} // DirectionsN is the highest valid value for type Directions, plus one. const DirectionsN Directions = 5 var _DirectionsValueMap = map[string]Directions{`ltr`: 0, `rtl`: 1, `ttb`: 2, `btt`: 3, `default`: 4} var _DirectionsDescMap = map[Directions]string{0: `LTR is Left-to-Right text.`, 1: `RTL is Right-to-Left text.`, 2: `TTB is Top-to-Bottom text.`, 3: `BTT is Bottom-to-Top text.`, 4: `Default uses the [text.Style] default direction.`} var _DirectionsMap = map[Directions]string{0: `ltr`, 1: `rtl`, 2: `ttb`, 3: `btt`, 4: `default`} // 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") } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rich import ( "cogentcore.org/core/text/textpos" ) // Hyperlink represents a hyperlink within shaped text. type Hyperlink struct { // Label is the text label for the link. Label string // URL is the full URL for the link. URL string // Properties are additional properties defined for the link, // e.g., from the parsed HTML attributes. TODO: resolve // Properties map[string]any // Range defines the starting and ending positions of the link, // in terms of source rune indexes. Range textpos.Range } // GetLinks gets all the links from the source. func (tx Text) GetLinks() []Hyperlink { var lks []Hyperlink n := len(tx) for si := range n { sp := RuneToSpecial(tx[si][0]) if sp != Link { continue } lr := tx.SpecialRange(si) if lr.End < 0 || lr.End <= lr.Start { continue } ls := tx[lr.Start:lr.End] s, _ := tx.Span(si) lk := Hyperlink{} lk.URL = s.URL sr, _ := tx.Range(lr.Start) _, er := tx.Range(lr.End) lk.Range = textpos.Range{sr, er} lk.Label = string(ls.Join()) lks = append(lks, lk) si = lr.End } return lks } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rich import ( "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/styles/styleprops" ) // FromProperties sets style field values based on the given property list. func (s *Style) FromProperties(parent *Style, 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 } s.FromProperty(parent, key, val, ctxt) } } // FromProperty sets style field values based on the given property key and value. func (s *Style) FromProperty(parent *Style, key string, val any, cc colors.Context) { if sfunc, ok := styleFuncs[key]; ok { if parent != nil { sfunc(s, key, val, parent, cc) } else { sfunc(s, key, val, nil, cc) } return } } // 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 FontSizes = map[string]float32{ "xx-small": 6.0 / 12.0, "x-small": 8.0 / 12.0, "small": 10.0 / 12.0, // small is also "smaller" "smallf": 10.0 / 12.0, // smallf = small font size.. "medium": 1, "large": 14.0 / 12.0, "x-large": 18.0 / 12.0, "xx-large": 24.0 / 12.0, } // styleFuncs are functions for styling the rich.Style object. var styleFuncs = map[string]styleprops.Func{ // note: text.Style handles the standard units-based font-size settings "font-size": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Style) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.Size = parent.(*Style).Size } else if init { fs.Size = 1.0 } return } switch vt := val.(type) { case string: if psz, ok := FontSizes[vt]; ok { fs.Size = psz } } }, "font-family": styleprops.Enum(SansSerif, func(obj *Style) enums.EnumSetter { return &obj.Family }), "font-style": styleprops.Enum(SlantNormal, func(obj *Style) enums.EnumSetter { return &obj.Slant }), "font-weight": styleprops.Enum(Normal, func(obj *Style) enums.EnumSetter { return &obj.Weight }), "font-stretch": styleprops.Enum(StretchNormal, func(obj *Style) enums.EnumSetter { return &obj.Stretch }), "text-decoration": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Style) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.Decoration = parent.(*Style).Decoration } else if init { fs.Decoration = 0 } return } switch vt := val.(type) { case string: if vt == "none" { fs.Decoration = 0 } else { fs.Decoration.SetString(vt) } case Decorations: fs.Decoration = vt default: iv, err := reflectx.ToInt(val) if err == nil { fs.Decoration = Decorations(iv) } else { styleprops.SetError(key, val, err) } } }, "direction": styleprops.Enum(LTR, func(obj *Style) enums.EnumSetter { return &obj.Direction }), "color": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Style) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.SetFillColor(parent.(*Style).FillColor()) } else if init { fs.SetFillColor(nil) } return } fs.SetFillColor(colors.ToUniform(errors.Log1(gradient.FromAny(val, cc)))) }, "stroke-color": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Style) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.SetStrokeColor(parent.(*Style).StrokeColor()) } else if init { fs.SetStrokeColor(nil) } return } fs.SetStrokeColor(colors.ToUniform(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 := styleprops.InhInit(val, parent); inh || init { if inh { fs.SetBackground(parent.(*Style).Background()) } else if init { fs.SetBackground(nil) } return } fs.SetBackground(colors.ToUniform(errors.Log1(gradient.FromAny(val, cc)))) }, "opacity": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Style) if inh, init := styleprops.InhInit(val, parent); inh || init { return } fv, _ := reflectx.ToFloat(val) if fv < 1 { if fs.background != nil { fs.background = colors.ApplyOpacity(fs.background, float32(fv)) } if fs.strokeColor != nil { fs.strokeColor = colors.ApplyOpacity(fs.strokeColor, float32(fv)) } if fs.fillColor != nil { fs.fillColor = colors.ApplyOpacity(fs.fillColor, float32(fv)) } } }, } // SetFromHTMLTag sets the styling parameters for simple HTML style tags. // Returns true if handled. func (s *Style) SetFromHTMLTag(tag string) bool { did := false switch tag { case "b", "strong": s.Weight = Bold did = true case "i", "em", "var", "cite": s.Slant = Italic did = true case "ins": fallthrough case "u": s.Decoration.SetFlag(true, Underline) did = true case "s", "del", "strike": s.Decoration.SetFlag(true, LineThrough) did = true case "small": s.Size = 0.8 did = true case "big": s.Size = 1.2 did = true case "xx-small", "x-small", "smallf", "medium", "large", "x-large", "xx-large": s.Size = FontSizes[tag] did = true case "mark": s.SetBackground(colors.ToUniform(colors.Scheme.Warn.Container)) did = true case "abbr", "acronym": s.Decoration.SetFlag(true, DottedUnderline) did = true case "tt", "kbd", "samp", "code": s.Family = Monospace s.SetBackground(colors.ToUniform(colors.Scheme.SurfaceContainer)) did = true } return did } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rich import ( "strings" "github.com/go-text/typesetting/language" ) func init() { DefaultSettings.Defaults() } // DefaultSettings contains the default global text settings. // This will be updated from rich.DefaultSettings. var DefaultSettings Settings // FontName is a special string that provides a font chooser. // It is aliased to [core.FontName] as well. type FontName string // Settings holds the global settings for rich text styling, // including language, script, and preferred font faces for // each category of font. type Settings struct { // Language is the preferred language used for rendering text. Language language.Language // Script is the specific writing system used for rendering text. // todo: no idea how to set this based on language or anything else. Script language.Script `display:"-"` // SansSerif is a font without serifs, where glyphs have plain stroke endings, // without ornamentation. Example sans-serif fonts include Arial, Helvetica, // Noto Sans, Open Sans, Fira Sans, Lucida Sans, Lucida Sans Unicode, Trebuchet MS, // Liberation Sans, Nimbus Sans L, Roboto. // This can be a list of comma-separated names, tried in order. // "sans-serif" will be added automatically as a final backup. SansSerif FontName `default:"Noto Sans"` // Serif is a small line or stroke attached to the end of a larger stroke // in a letter. In serif fonts, glyphs have finishing strokes, flared or // tapering ends. Examples include Times New Roman, Lucida Bright, // Lucida Fax, Palatino, Palatino Linotype, Palladio, and URW Palladio. // This can be a list of comma-separated names, tried in order. // "serif" will be added automatically as a final backup. Serif FontName // Monospace fonts have all glyphs with he same fixed width. // Example monospace fonts include Roboto Mono, Fira Mono, DejaVu Sans Mono, // Menlo, Consolas, Liberation Mono, Monaco, and Lucida Console. // This can be a list of comma-separated names. serif will be added // automatically as a final backup. // This can be a list of comma-separated names, tried in order. // "monospace" will be added automatically as a final backup. Monospace FontName `default:"Roboto Mono"` // Cursive glyphs generally have either joining strokes or other cursive // characteristics beyond those of italic typefaces. The glyphs are partially // or completely connected, and the result looks more like handwritten pen or // brush writing than printed letter work. Example cursive fonts include // Brush Script MT, Brush Script Std, Lucida Calligraphy, Lucida Handwriting, // and Apple Chancery. // This can be a list of comma-separated names, tried in order. // "cursive" will be added automatically as a final backup. Cursive FontName // Fantasy fonts are primarily decorative fonts that contain playful // representations of characters. Example fantasy fonts include Papyrus, // Herculanum, Party LET, Curlz MT, and Harrington. // This can be a list of comma-separated names, tried in order. // "fantasy" will be added automatically as a final backup. Fantasy FontName // Math fonts are for displaying mathematical expressions, for example // superscript and subscript, brackets that cross several lines, nesting // expressions, and double-struck glyphs with distinct meanings. // This can be a list of comma-separated names, tried in order. // "math" will be added automatically as a final backup. Math FontName // Emoji fonts are specifically designed to render emoji. // This can be a list of comma-separated names, tried in order. // "emoji" will be added automatically as a final backup. Emoji FontName // Fangsong are a particular style of Chinese characters that are between // serif-style Song and cursive-style Kai forms. This style is often used // for government documents. // This can be a list of comma-separated names, tried in order. // "fangsong" will be added automatically as a final backup. Fangsong FontName } func (rts *Settings) Defaults() { rts.Language = language.DefaultLanguage() rts.SansSerif = "Noto Sans" rts.Monospace = "Roboto Mono" } // AddFamily adds a family specifier to the given font string, // handling the comma properly. func AddFamily(rts FontName, fam string) string { if rts == "" { return fam } s := string(rts) // if strings.Contains(s, " ") { // no! this is bad // s = `"` + s + `"` // } return s + ", " + fam } // FamiliesToList returns a list of the families, split by comma and space removed. func FamiliesToList(fam string) []string { fs := strings.Split(fam, ",") os := make([]string, 0, len(fs)) for _, f := range fs { rts := strings.TrimSpace(f) if rts == "" { continue } os = append(os, rts) } return os } // Family returns the font family specified by the given [Family] enum. func (rts *Settings) Family(fam Family) string { switch fam { case SansSerif: return AddFamily(rts.SansSerif, `-apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif, emoji`) case Serif: return AddFamily(rts.Serif, `serif, emoji`) case Monospace: return AddFamily(rts.Monospace, `monospace, emoji`) case Cursive: return AddFamily(rts.Cursive, `cursive, emoji`) case Fantasy: return AddFamily(rts.Fantasy, `fantasy, emoji`) case Math: return AddFamily(rts.Math, "math") case Emoji: return AddFamily(rts.Emoji, "emoji") case Fangsong: return AddFamily(rts.Fangsong, "fangsong") } return "sans-serif" } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rich import ( "image/color" "math" ) // srune is a uint32 rune value that encodes the font styles. // There is no attempt to pack these values into the Private Use Areas // of unicode, because they are never encoded into the unicode directly. // Because we have the room, we use at least 4 bits = 1 hex F for each // element of the style property. Size and Color values are added after // the main style rune element. // RuneFromStyle returns the style rune that encodes the given style values. func RuneFromStyle(s *Style) rune { return RuneFromDecoration(s.Decoration) | RuneFromSpecial(s.Special) | RuneFromStretch(s.Stretch) | RuneFromWeight(s.Weight) | RuneFromSlant(s.Slant) | RuneFromFamily(s.Family) | RuneFromDirection(s.Direction) } // RuneToStyle sets all the style values decoded from given rune. func RuneToStyle(s *Style, r rune) { s.Decoration = RuneToDecoration(r) s.Special = RuneToSpecial(r) s.Stretch = RuneToStretch(r) s.Weight = RuneToWeight(r) s.Slant = RuneToSlant(r) s.Family = RuneToFamily(r) s.Direction = RuneToDirection(r) } // SpanLen returns the length of the starting style runes and // following content runes for given slice of span runes. // Does not need to decode full style, so is very efficient. func SpanLen(s []rune) (sn int, rn int) { r0 := s[0] nc := RuneToDecoration(r0).NumColors() sn = 2 + nc // style + size + nc isLink := RuneToSpecial(r0) == Link if !isLink { rn = max(0, len(s)-sn) return } ln := int(s[sn]) // link len sn += ln + 1 rn = max(0, len(s)-sn) return } // FromRunes sets the Style properties from the given rune encodings // which must be the proper length including colors. Any remaining // runes after the style runes are returned: this is the source string. func (s *Style) FromRunes(rs []rune) []rune { RuneToStyle(s, rs[0]) s.Size = math.Float32frombits(uint32(rs[1])) ci := 2 if s.Decoration.HasFlag(FillColor) { s.fillColor = ColorFromRune(rs[ci]) ci++ } if s.Decoration.HasFlag(StrokeColor) { s.strokeColor = ColorFromRune(rs[ci]) ci++ } if s.Decoration.HasFlag(Background) { s.background = ColorFromRune(rs[ci]) ci++ } if s.Special == Link { ln := int(rs[ci]) ci++ s.URL = string(rs[ci : ci+ln]) ci += ln } if ci < len(rs) { return rs[ci:] } return nil } // ToRunes returns the rune(s) that encode the given style // including any additional colors beyond the style and size runes, // and the URL for a link. func (s *Style) ToRunes() []rune { r := RuneFromStyle(s) rs := []rune{r, rune(math.Float32bits(s.Size))} if s.Decoration.NumColors() == 0 { return rs } if s.Decoration.HasFlag(FillColor) { rs = append(rs, ColorToRune(s.fillColor)) } if s.Decoration.HasFlag(StrokeColor) { rs = append(rs, ColorToRune(s.strokeColor)) } if s.Decoration.HasFlag(Background) { rs = append(rs, ColorToRune(s.background)) } if s.Special == Link { rs = append(rs, rune(len(s.URL))) rs = append(rs, []rune(s.URL)...) } return rs } // ColorToRune converts given color to a rune uint32 value. func ColorToRune(c color.Color) rune { r, g, b, a := c.RGBA() // uint32 r8 := r >> 8 g8 := g >> 8 b8 := b >> 8 a8 := a >> 8 return rune(r8<<24) + rune(g8<<16) + rune(b8<<8) + rune(a8) } // ColorFromRune converts given color from a rune uint32 value. func ColorFromRune(r rune) color.RGBA { ru := uint32(r) r8 := uint8((ru & 0xFF000000) >> 24) g8 := uint8((ru & 0x00FF0000) >> 16) b8 := uint8((ru & 0x0000FF00) >> 8) a8 := uint8((ru & 0x000000FF)) return color.RGBA{r8, g8, b8, a8} } const ( DecorationStart = 0 DecorationMask = 0x000007FF // 11 bits reserved for deco SlantStart = 11 SlantMask = 0x00000800 // 1 bit for slant SpecialStart = 12 SpecialMask = 0x0000F000 StretchStart = 16 StretchMask = 0x000F0000 WeightStart = 20 WeightMask = 0x00F00000 FamilyStart = 24 FamilyMask = 0x0F000000 DirectionStart = 28 DirectionMask = 0xF0000000 ) // RuneFromDecoration returns the rune bit values for given decoration. func RuneFromDecoration(d Decorations) rune { return rune(d) } // RuneToDecoration returns the Decoration bit values from given rune. func RuneToDecoration(r rune) Decorations { return Decorations(uint32(r) & DecorationMask) } // RuneFromSpecial returns the rune bit values for given special. func RuneFromSpecial(d Specials) rune { return rune(d << SpecialStart) } // RuneToSpecial returns the Specials value from given rune. func RuneToSpecial(r rune) Specials { return Specials((uint32(r) & SpecialMask) >> SpecialStart) } // RuneFromStretch returns the rune bit values for given stretch. func RuneFromStretch(d Stretch) rune { return rune(d << StretchStart) } // RuneToStretch returns the Stretch value from given rune. func RuneToStretch(r rune) Stretch { return Stretch((uint32(r) & StretchMask) >> StretchStart) } // RuneFromWeight returns the rune bit values for given weight. func RuneFromWeight(d Weights) rune { return rune(d << WeightStart) } // RuneToWeight returns the Weights value from given rune. func RuneToWeight(r rune) Weights { return Weights((uint32(r) & WeightMask) >> WeightStart) } // RuneFromSlant returns the rune bit values for given slant. func RuneFromSlant(d Slants) rune { return rune(d << SlantStart) } // RuneToSlant returns the Slants value from given rune. func RuneToSlant(r rune) Slants { return Slants((uint32(r) & SlantMask) >> SlantStart) } // RuneFromFamily returns the rune bit values for given family. func RuneFromFamily(d Family) rune { return rune(d << FamilyStart) } // RuneToFamily returns the Familys value from given rune. func RuneToFamily(r rune) Family { return Family((uint32(r) & FamilyMask) >> FamilyStart) } // RuneFromDirection returns the rune bit values for given direction. func RuneFromDirection(d Directions) rune { return rune(d << DirectionStart) } // RuneToDirection returns the Directions value from given rune. func RuneToDirection(r rune) Directions { return Directions((uint32(r) & DirectionMask) >> DirectionStart) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rich import ( "fmt" "image/color" "strings" "cogentcore.org/core/colors" "github.com/go-text/typesetting/di" ) //go:generate core generate // IMPORTANT: enums must remain in sync with // "github.com/go-text/typesetting/font" // and props.go must be updated as needed. // Style contains all of the rich text styling properties, that apply to one // span of text. These are encoded into a uint32 rune value in [rich.Text]. // See [text.Style] and [Settings] for additional context needed for full specification. type Style struct { //types:add -setters // Size is the font size multiplier relative to the standard font size // specified in the [text.Style]. Size float32 // Family indicates the generic family of typeface to use, where the // specific named values to use for each are provided in the [Settings], // or [text.Style] for [Custom]. Family Family // Slant allows italic or oblique faces to be selected. Slant Slants // Weights are the degree of blackness or stroke thickness of a font. // This value ranges from 100.0 to 900.0, with 400.0 as normal. Weight Weights // Stretch is the width of a font as an approximate fraction of the normal width. // Widths range from 0.5 to 2.0 inclusive, with 1.0 as the normal width. Stretch Stretch // Special additional formatting factors that are not otherwise // captured by changes in font rendering properties or decorations. // See [Specials] for usage information: use [Text.StartSpecial] // and [Text.EndSpecial] to set. Special Specials // Decorations are underline, line-through, etc, as bit flags // that must be set using [Decorations.SetFlag]. Decoration Decorations `set:"-"` // Direction is the direction to render the text. Direction Directions // fillColor is the color to use for glyph fill (i.e., the standard "ink" color). // Must use SetFillColor to set Decoration FillColor flag. // This will be encoded in a uint32 following the style rune, in rich.Text spans. fillColor color.Color // strokeColor is the color to use for glyph outline stroking. // Must use SetStrokeColor to set Decoration StrokeColor flag. // This will be encoded in a uint32 following the style rune, in rich.Text spans. strokeColor color.Color // background is the color to use for the background region. // Must use SetBackground to set the Decoration Background flag. // This will be encoded in a uint32 following the style rune, in rich.Text spans. background color.Color `set:"-"` // URL is the URL for a link element. It is encoded in runes after the style runes. URL string } func NewStyle() *Style { s := &Style{} s.Defaults() return s } // Clone returns a copy of this style. func (s *Style) Clone() *Style { ns := &Style{} *ns = *s return ns } // NewStyleFromRunes returns a new style initialized with data from given runes, // returning the remaining actual rune string content after style data. func NewStyleFromRunes(rs []rune) (*Style, []rune) { s := NewStyle() c := s.FromRunes(rs) return s, c } func (s *Style) Defaults() { s.Size = 1 s.Weight = Normal s.Stretch = StretchNormal s.Direction = Default } // InheritFields from parent func (s *Style) InheritFields(parent *Style) { // fs.Color = par.Color s.Family = parent.Family s.Slant = parent.Slant if parent.Size != 0 { s.Size = parent.Size } s.Weight = parent.Weight s.Stretch = parent.Stretch } // FontFamily returns the font family name(s) based on [Style.Family] and the // values specified in the given [Settings]. func (s *Style) FontFamily(ctx *Settings) string { return ctx.Family(s.Family) } // Family specifies the generic family of typeface to use, where the // specific named values to use for each are provided in the Settings. type Family int32 //enums:enum -trim-prefix Family -transform kebab const ( // SansSerif is a font without serifs, where glyphs have plain stroke endings, // without ornamentation. Example sans-serif fonts include Arial, Helvetica, // Open Sans, Fira Sans, Lucida Sans, Lucida Sans Unicode, Trebuchet MS, // Liberation Sans, and Nimbus Sans L. SansSerif Family = iota // Serif is a small line or stroke attached to the end of a larger stroke // in a letter. In serif fonts, glyphs have finishing strokes, flared or // tapering ends. Examples include Times New Roman, Lucida Bright, // Lucida Fax, Palatino, Palatino Linotype, Palladio, and URW Palladio. Serif // Monospace fonts have all glyphs with he same fixed width. // Example monospace fonts include Fira Mono, DejaVu Sans Mono, // Menlo, Consolas, Liberation Mono, Monaco, and Lucida Console. Monospace // Cursive glyphs generally have either joining strokes or other cursive // characteristics beyond those of italic typefaces. The glyphs are partially // or completely connected, and the result looks more like handwritten pen or // brush writing than printed letter work. Example cursive fonts include // Brush Script MT, Brush Script Std, Lucida Calligraphy, Lucida Handwriting, // and Apple Chancery. Cursive // Fantasy fonts are primarily decorative fonts that contain playful // representations of characters. Example fantasy fonts include Papyrus, // Herculanum, Party LET, Curlz MT, and Harrington. Fantasy // Math fonts are for displaying mathematical expressions, for example // superscript and subscript, brackets that cross several lines, nesting // expressions, and double-struck glyphs with distinct meanings. Math // Emoji fonts are specifically designed to render emoji. Emoji // Fangsong are a particular style of Chinese characters that are between // serif-style Song and cursive-style Kai forms. This style is often used // for government documents. Fangsong // Custom is a custom font name that is specified in the [text.Style] // CustomFont name. Custom ) // Slants (also called style) allows italic or oblique faces to be selected. type Slants int32 //enums:enum -trim-prefix Slant -transform kebab const ( // A face that is neither italic not obliqued. SlantNormal Slants = iota // A form that is generally cursive in nature or slanted. // This groups what is usually called Italic or Oblique. Italic ) // Weights are the degree of blackness or stroke thickness of a font. // The corresponding value ranges from 100.0 to 900.0, with 400.0 as normal. type Weights int32 //enums:enum -transform kebab const ( // Thin weight (100), the thinnest value. Thin Weights = iota // Extra light weight (200). ExtraLight // Light weight (300). Light // Normal (400). Normal // Medium weight (500, higher than normal). Medium // Semibold weight (600). Semibold // Bold weight (700). Bold // Extra-bold weight (800). ExtraBold // Black weight (900), the thickest value. Black ) // ToFloat32 converts the weight to its numerical 100x value func (w Weights) ToFloat32() float32 { return float32((w + 1) * 100) } func (w Weights) HTMLTag() string { switch w { case Bold: return "b" } return "" } // Stretch is the width of a font as an approximate fraction of the normal width. // Widths range from 0.5 to 2.0 inclusive, with 1.0 as the normal width. type Stretch int32 //enums:enum -trim-prefix Stretch -transform kebab const ( // Ultra-condensed width (50%), the narrowest possible. UltraCondensed Stretch = iota // Extra-condensed width (62.5%). ExtraCondensed // Condensed width (75%). Condensed // Semi-condensed width (87.5%). SemiCondensed // Normal width (100%). StretchNormal // Semi-expanded width (112.5%). SemiExpanded // Expanded width (125%). Expanded // Extra-expanded width (150%). ExtraExpanded // Ultra-expanded width (200%), the widest possible. UltraExpanded ) var stretchFloatValues = []float32{0.5, 0.625, 0.75, 0.875, 1, 1.125, 1.25, 1.5, 2.0} // ToFloat32 converts the stretch to its numerical multiplier value func (s Stretch) ToFloat32() float32 { return stretchFloatValues[s] } // note: 11 bits reserved, 8 used // Decorations are underline, line-through, etc, as bit flags // that must be set using [Font.SetDecoration]. type Decorations int64 //enums:bitflag -transform kebab const ( // Underline indicates to place a line below text. Underline Decorations = iota // Overline indicates to place a line above text. Overline // LineThrough indicates to place a line through text. LineThrough // DottedUnderline is used for abbr tag. DottedUnderline // ParagraphStart indicates that this text is the start of a paragraph, // and therefore may be indented according to [text.Style] settings. ParagraphStart // FillColor means that the fill color of the glyph is set. // The standard font rendering uses this fill color (compare to StrokeColor). FillColor // StrokeColor means that the stroke color of the glyph is set. // This is normally not rendered: it looks like an outline of the glyph at // larger font sizes, and will make smaller font sizes look significantly thicker. StrokeColor // Background means that the background region behind the text is colored. // The background is not normally colored so it renders over any background. Background ) // NumColors returns the number of colors used by this decoration setting. func (d Decorations) NumColors() int { nc := 0 if d.HasFlag(FillColor) { nc++ } if d.HasFlag(StrokeColor) { nc++ } if d.HasFlag(Background) { nc++ } return nc } // Specials are special additional mutually exclusive formatting factors that are not // otherwise captured by changes in font rendering properties or decorations. // Each special must be terminated by an End span element, on its own, which // pops the stack on the last special that was started. // Use [Text.StartSpecial] and [Text.EndSpecial] to manage the specials, // avoiding the potential for repeating the start of a given special. type Specials int32 //enums:enum -transform kebab const ( // Nothing special. Nothing Specials = iota // Super starts super-scripted text. Super // Sub starts sub-scripted text. Sub // Link starts a hyperlink, which is in the URL field of the // style, and encoded in the runes after the style runes. // It also identifies this span for functional interactions // such as hovering and clicking. It does not specify the styling, // which therefore must be set in addition. Link // MathInline starts a TeX formatted math sequence, styled for // inclusion inline with other text. MathInline // MathDisplay starts a TeX formatted math sequence, styled as // a larger standalone display. MathDisplay // Quote starts an indented paragraph-level quote. Quote // todo: could add SmallCaps here? // End must be added to terminate the last Special started: use [Text.AddEnd]. // The renderer maintains a stack of special elements. End ) // Directions specifies the text layout direction. type Directions int32 //enums:enum -transform kebab const ( // LTR is Left-to-Right text. LTR Directions = iota // RTL is Right-to-Left text. RTL // TTB is Top-to-Bottom text. TTB // BTT is Bottom-to-Top text. BTT // Default uses the [text.Style] default direction. Default ) // ToGoText returns the go-text version of direction. func (d Directions) ToGoText() di.Direction { return di.Direction(d) } // IsVertical returns true if given text is vertical. func (d Directions) IsVertical() bool { return d >= TTB && d <= BTT } // SetDecoration sets given decoration flag(s) on. func (s *Style) SetDecoration(deco ...Decorations) *Style { for _, d := range deco { s.Decoration.SetFlag(true, d) } return s } func (s *Style) FillColor() color.Color { return s.fillColor } func (s *Style) StrokeColor() color.Color { return s.strokeColor } func (s *Style) Background() color.Color { return s.background } // SetFillColor sets the fill color to given color, setting the Decoration // flag and the color value. func (s *Style) SetFillColor(clr color.Color) *Style { s.fillColor = clr s.Decoration.SetFlag(clr != nil, FillColor) return s } // SetStrokeColor sets the stroke color to given color, setting the Decoration // flag and the color value. // This is normally not set: it looks like an outline of the glyph at // larger font sizes, and will make smaller font sizes look significantly thicker. func (s *Style) SetStrokeColor(clr color.Color) *Style { s.strokeColor = clr s.Decoration.SetFlag(clr != nil, StrokeColor) return s } // SetBackground sets the background color to given color, setting the Decoration // flag and the color value. // The background is not normally colored so it renders over any background. func (s *Style) SetBackground(clr color.Color) *Style { s.background = clr s.Decoration.SetFlag(clr != nil, Background) return s } // SetLinkStyle sets the default hyperlink styling: primary.Base color (e.g., blue) // and Underline. func (s *Style) SetLinkStyle() *Style { s.SetFillColor(colors.ToUniform(colors.Scheme.Primary.Base)) s.Decoration.SetFlag(true, Underline) return s } // SetLink sets the given style as a hyperlink, with given URL, and // default link styling. func (s *Style) SetLink(url string) *Style { s.URL = url s.Special = Link return s.SetLinkStyle() } // IsMath returns true if is a Special MathInline or MathDisplay. func (s *Style) IsMath() bool { return s.Special == MathInline || s.Special == MathDisplay } func (s *Style) String() string { str := "" if s.Special == End { return "{End Special}" } if s.Size != 1 { str += fmt.Sprintf("%5.2fx ", s.Size) } if s.Family != SansSerif { str += s.Family.String() + " " } if s.Slant != SlantNormal { str += s.Slant.String() + " " } if s.Weight != Normal { str += s.Weight.String() + " " } if s.Stretch != StretchNormal { str += s.Stretch.String() + " " } if s.Special != Nothing { str += s.Special.String() + " " if s.Special == Link { str += "[" + s.URL + "] " } } for d := Underline; d <= Background; d++ { if s.Decoration.HasFlag(d) { str += d.BitIndexString() + " " } } return strings.TrimSpace(str) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rich import ( "fmt" "slices" "unicode" "cogentcore.org/core/text/textpos" ) // Text is the basic rich text representation, with spans of []rune unicode characters // that share a common set of text styling properties, which are represented // by the first rune(s) in each span. If custom colors are used, they are encoded // after the first style and size runes. // This compact and efficient representation can be Join'd back into the raw // unicode source, and indexing by rune index in the original is fast. // It provides a GPU-compatible representation, and is the text equivalent of // the [ppath.Path] encoding. type Text [][]rune // NewText returns a new [Text] starting with given style and runes string, // which can be empty. func NewText(s *Style, r []rune) Text { tx := Text{} tx.AddSpan(s, r) return tx } // NewPlainText returns a new [Text] starting with default style and runes string, // which can be empty. func NewPlainText(r []rune) Text { return NewText(NewStyle(), r) } // NumSpans returns the number of spans in this Text. func (tx Text) NumSpans() int { return len(tx) } // Len returns the total number of runes in this Text. func (tx Text) Len() int { n := 0 for _, s := range tx { _, rn := SpanLen(s) n += rn } return n } // Range returns the start, end range of indexes into original source // for given span index. func (tx Text) Range(span int) (start, end int) { ci := 0 for si, s := range tx { _, rn := SpanLen(s) if si == span { return ci, ci + rn } ci += rn } return -1, -1 } // Index returns the span index, number of style runes at start of span, // and index into actual runes within the span after style runes, // for the given logical index into the original source rune slice // without spans or styling elements. // If the logical index is invalid for the text returns -1,-1,-1. func (tx Text) Index(li int) (span, stylen, ridx int) { ci := 0 for si, s := range tx { sn, rn := SpanLen(s) if li >= ci && li < ci+rn { return si, sn, sn + (li - ci) } ci += rn } return -1, -1, -1 } // AtTry returns the rune at given logical index, as in the original // source rune slice without any styling elements. Returns 0 // and false if index is invalid. func (tx Text) AtTry(li int) (rune, bool) { ci := 0 for _, s := range tx { sn, rn := SpanLen(s) if li >= ci && li < ci+rn { return s[sn+(li-ci)], true } ci += rn } return -1, false } // At returns the rune at given logical index into the original // source rune slice without any styling elements. Returns 0 // if index is invalid. See AtTry for a version that also returns a bool // indicating whether the index is valid. func (tx Text) At(li int) rune { r, _ := tx.AtTry(li) return r } // Split returns the raw rune spans without any styles. // The rune span slices here point directly into the Text rune slices. // See SplitCopy for a version that makes a copy instead. func (tx Text) Split() [][]rune { ss := make([][]rune, 0, len(tx)) for _, s := range tx { sn, _ := SpanLen(s) ss = append(ss, s[sn:]) } return ss } // SplitCopy returns the raw rune spans without any styles. // The rune span slices here are new copies; see also [Text.Split]. func (tx Text) SplitCopy() [][]rune { ss := make([][]rune, 0, len(tx)) for _, s := range tx { sn, _ := SpanLen(s) ss = append(ss, slices.Clone(s[sn:])) } return ss } // Join returns a single slice of runes with the contents of all span runes. func (tx Text) Join() []rune { ss := make([]rune, 0, tx.Len()) for _, s := range tx { sn, _ := SpanLen(s) if sn < len(s) { ss = append(ss, s[sn:]...) } } return ss } // Span returns the [Style] and []rune content for given span index. // Returns nil if out of range. func (tx Text) Span(si int) (*Style, []rune) { n := len(tx) if si < 0 || si >= n || len(tx[si]) == 0 { return nil, nil } return NewStyleFromRunes(tx[si]) } // SetSpanStyle sets the style for given span, updating the runes to encode it. func (tx *Text) SetSpanStyle(si int, nsty *Style) *Text { sty, r := tx.Span(si) *sty = *nsty nr := sty.ToRunes() nr = append(nr, r...) (*tx)[si] = nr return tx } // SetSpanRunes sets the runes for given span. func (tx *Text) SetSpanRunes(si int, r []rune) *Text { sty, _ := tx.Span(si) nr := sty.ToRunes() nr = append(nr, r...) (*tx)[si] = nr return tx } // AddSpan adds a span to the Text using the given Style and runes. // The Text is modified for convenience in the high-frequency use-case. // Clone first to avoid changing the original. func (tx *Text) AddSpan(s *Style, r []rune) *Text { nr := s.ToRunes() nr = append(nr, r...) *tx = append(*tx, nr) return tx } // AddSpanString adds a span to the Text using the given Style and string content. // The Text is modified for convenience in the high-frequency use-case. // Clone first to avoid changing the original. func (tx *Text) AddSpanString(s *Style, r string) *Text { nr := s.ToRunes() nr = append(nr, []rune(r)...) *tx = append(*tx, nr) return tx } // InsertSpan inserts a span to the Text at given span index, // using the given Style and runes. // The Text is modified for convenience in the high-frequency use-case. // Clone first to avoid changing the original. func (tx *Text) InsertSpan(at int, s *Style, r []rune) *Text { nr := s.ToRunes() nr = append(nr, r...) *tx = slices.Insert(*tx, at, nr) return tx } // SplitSpan splits an existing span at the given logical source index, // with the span containing that logical index truncated to contain runes // just before the index, and a new span inserted starting at that index, // with the remaining contents of the original containing span. // If that logical index is already the start of a span, or the logical // index is invalid, nothing happens. Returns the index of span, // which will be negative if the logical index is out of range. func (tx *Text) SplitSpan(li int) int { si, sn, ri := tx.Index(li) if si < 0 { return si } if sn == ri { // already the start return si } nr := slices.Clone((*tx)[si][:sn]) // style runes nr = append(nr, (*tx)[si][ri:]...) (*tx)[si] = (*tx)[si][:ri] // truncate *tx = slices.Insert(*tx, si+1, nr) return si + 1 } // StartSpecial adds a Span of given Special type to the Text, // using given style and rune text. This creates a new style // with the special value set, to avoid accidentally repeating // the start of new specials. func (tx *Text) StartSpecial(s *Style, special Specials, r []rune) *Text { ss := *s ss.Special = special return tx.AddSpan(&ss, r) } // EndSpecial adds an [End] Special to the Text, to terminate the current // Special. All [Specials] must be terminated with this empty end tag. func (tx *Text) EndSpecial() *Text { s := NewStyle() s.Special = End return tx.AddSpan(s, nil) } // InsertEndSpecial inserts an [End] Special to the Text at given span // index, to terminate the current Special. All [Specials] must be // terminated with this empty end tag. func (tx *Text) InsertEndSpecial(at int) *Text { s := NewStyle() s.Special = End return tx.InsertSpan(at, s, nil) } // SpecialRange returns the range of spans for the // special starting at given span index. Returns -1 if span // at given index is not a special. func (tx Text) SpecialRange(si int) textpos.Range { sp := RuneToSpecial(tx[si][0]) if sp == Nothing { return textpos.Range{-1, -1} } depth := 1 n := len(tx) for j := si + 1; j < n; j++ { s := RuneToSpecial(tx[j][0]) switch s { case End: depth-- if depth == 0 { return textpos.Range{si, j} } default: depth++ } } return textpos.Range{-1, -1} } // AddLink adds a [Link] special with given url and label text. // This calls StartSpecial and EndSpecial for you. If the link requires // further formatting, use those functions separately. func (tx *Text) AddLink(s *Style, url, label string) *Text { ss := *s ss.URL = url tx.StartSpecial(&ss, Link, []rune(label)) return tx.EndSpecial() } // AddSuper adds a [Super] special with given text. // This calls StartSpecial and EndSpecial for you. If the Super requires // further formatting, use those functions separately. func (tx *Text) AddSuper(s *Style, text string) *Text { tx.StartSpecial(s, Super, []rune(text)) return tx.EndSpecial() } // AddSub adds a [Sub] special with given text. // This calls StartSpecial and EndSpecial for you. If the Sub requires // further formatting, use those functions separately. func (tx *Text) AddSub(s *Style, text string) *Text { tx.StartSpecial(s, Sub, []rune(text)) return tx.EndSpecial() } // AddMathInline adds a [MathInline] special with given text. // This calls StartSpecial and EndSpecial for you. If the Math requires // further formatting, use those functions separately. func (tx *Text) AddMathInline(s *Style, text string) *Text { tx.StartSpecial(s, MathInline, []rune(text)) return tx.EndSpecial() } // AddMathDisplay adds a [MathDisplay] special with given text. // This calls StartSpecial and EndSpecial for you. If the Math requires // further formatting, use those functions separately. func (tx *Text) AddMathDisplay(s *Style, text string) *Text { tx.StartSpecial(s, MathDisplay, []rune(text)) return tx.EndSpecial() } // AddRunes adds given runes to current span. // If no existing span, then a new default one is made. func (tx *Text) AddRunes(r []rune) *Text { n := len(*tx) if n == 0 { return tx.AddSpan(NewStyle(), r) } (*tx)[n-1] = append((*tx)[n-1], r...) return tx } func (tx Text) String() string { str := "" for _, rs := range tx { s := &Style{} ss := s.FromRunes(rs) sstr := s.String() str += "[" + sstr + "]: \"" + string(ss) + "\"\n" } return str } // Join joins multiple texts into one text. Just appends the spans. func Join(txts ...Text) Text { nt := Text{} for _, tx := range txts { nt = append(nt, tx...) } return nt } func (tx Text) DebugDump() { for i := range tx { s, r := tx.Span(i) fmt.Println(i, len(tx[i]), tx[i]) fmt.Printf("style: %#v\n", s) fmt.Printf("chars: %q\n", string(r)) } } // Clone returns a deep copy clone of the current text, safe for subsequent // modification without affecting this one. func (tx Text) Clone() Text { ct := make(Text, len(tx)) for i := range tx { ct[i] = slices.Clone(tx[i]) } return ct } // SplitSpaces splits this text after first unicode space after non-space. func (tx *Text) SplitSpaces() { txt := tx.Join() if len(txt) == 0 { return } prevSp := unicode.IsSpace(txt[0]) for i, r := range txt { isSp := unicode.IsSpace(r) if prevSp && isSp { continue } if isSp { prevSp = true tx.SplitSpan(i + 1) // already doesn't re-split } else { prevSp = false } } } // Code generated by "core generate"; DO NOT EDIT. package rich import ( "cogentcore.org/core/types" ) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/rich.Style", IDName: "style", Doc: "Style contains all of the rich text styling properties, that apply to one\nspan of text. These are encoded into a uint32 rune value in [rich.Text].\nSee [text.Style] and [Settings] for additional context needed for full specification.", Directives: []types.Directive{{Tool: "go", Directive: "generate", Args: []string{"core", "generate"}}, {Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Fields: []types.Field{{Name: "Size", Doc: "Size is the font size multiplier relative to the standard font size\nspecified in the [text.Style]."}, {Name: "Family", Doc: "Family indicates the generic family of typeface to use, where the\nspecific named values to use for each are provided in the [Settings],\nor [text.Style] for [Custom]."}, {Name: "Slant", Doc: "Slant allows italic or oblique faces to be selected."}, {Name: "Weight", Doc: "Weights are the degree of blackness or stroke thickness of a font.\nThis value ranges from 100.0 to 900.0, with 400.0 as normal."}, {Name: "Stretch", Doc: "Stretch is the width of a font as an approximate fraction of the normal width.\nWidths range from 0.5 to 2.0 inclusive, with 1.0 as the normal width."}, {Name: "Special", Doc: "Special additional formatting factors that are not otherwise\ncaptured by changes in font rendering properties or decorations.\nSee [Specials] for usage information: use [Text.StartSpecial]\nand [Text.EndSpecial] to set."}, {Name: "Decoration", Doc: "Decorations are underline, line-through, etc, as bit flags\nthat must be set using [Decorations.SetFlag]."}, {Name: "Direction", Doc: "Direction is the direction to render the text."}, {Name: "fillColor", Doc: "fillColor is the color to use for glyph fill (i.e., the standard \"ink\" color).\nMust use SetFillColor to set Decoration FillColor flag.\nThis will be encoded in a uint32 following the style rune, in rich.Text spans."}, {Name: "strokeColor", Doc: "strokeColor is the color to use for glyph outline stroking.\nMust use SetStrokeColor to set Decoration StrokeColor flag.\nThis will be encoded in a uint32 following the style rune, in rich.Text spans."}, {Name: "background", Doc: "background is the color to use for the background region.\nMust use SetBackground to set the Decoration Background flag.\nThis will be encoded in a uint32 following the style rune, in rich.Text spans."}, {Name: "URL", Doc: "URL is the URL for a link element. It is encoded in runes after the style runes."}}}) // SetSize sets the [Style.Size]: // Size is the font size multiplier relative to the standard font size // specified in the [text.Style]. func (t *Style) SetSize(v float32) *Style { t.Size = v; return t } // SetFamily sets the [Style.Family]: // Family indicates the generic family of typeface to use, where the // specific named values to use for each are provided in the [Settings], // or [text.Style] for [Custom]. func (t *Style) SetFamily(v Family) *Style { t.Family = v; return t } // SetSlant sets the [Style.Slant]: // Slant allows italic or oblique faces to be selected. func (t *Style) SetSlant(v Slants) *Style { t.Slant = v; return t } // SetWeight sets the [Style.Weight]: // Weights are the degree of blackness or stroke thickness of a font. // This value ranges from 100.0 to 900.0, with 400.0 as normal. func (t *Style) SetWeight(v Weights) *Style { t.Weight = v; return t } // SetStretch sets the [Style.Stretch]: // Stretch is the width of a font as an approximate fraction of the normal width. // Widths range from 0.5 to 2.0 inclusive, with 1.0 as the normal width. func (t *Style) SetStretch(v Stretch) *Style { t.Stretch = v; return t } // SetSpecial sets the [Style.Special]: // Special additional formatting factors that are not otherwise // captured by changes in font rendering properties or decorations. // See [Specials] for usage information: use [Text.StartSpecial] // and [Text.EndSpecial] to set. func (t *Style) SetSpecial(v Specials) *Style { t.Special = v; return t } // SetDirection sets the [Style.Direction]: // Direction is the direction to render the text. func (t *Style) SetDirection(v Directions) *Style { t.Direction = v; return t } // SetURL sets the [Style.URL]: // URL is the URL for a link element. It is encoded in runes after the style runes. func (t *Style) SetURL(v string) *Style { t.URL = 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 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" ) // 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 } const maxInt = int(^uint(0) >> 1) // Equal reports whether a and b // are the same length and contain the same bytes. // A nil argument is equivalent to an empty slice. func Equal(a, b []rune) bool { // Neither cmd/compile nor gccgo allocates for these string conversions. return string(a) == string(b) } // // Compare returns an integer comparing two byte slices lexicographically. // // The result will be 0 if a == b, -1 if a < b, and +1 if a > b. // // A nil argument is equivalent to an empty slice. // func Compare(a, b []rune) int { // return bytealg.Compare(a, b) // } // Count counts the number of non-overlapping instances of sep in s. // If sep is an empty slice, Count returns 1 + the number of UTF-8-encoded code points in s. func Count(s, sep []rune) int { n := 0 for { i := Index(s, sep) if i == -1 { return n } n++ s = s[i+len(sep):] } } // Contains reports whether subslice is within b. func Contains(b, subslice []rune) bool { return Index(b, subslice) != -1 } // ContainsRune reports whether the rune is contained in the UTF-8-encoded byte slice b. func ContainsRune(b []rune, r rune) bool { return Index(b, []rune{r}) >= 0 // return IndexRune(b, r) >= 0 } // ContainsFunc reports whether any of the UTF-8-encoded code points r within b satisfy f(r). func ContainsFunc(b []rune, f func(rune) bool) bool { return IndexFunc(b, f) >= 0 } // containsRune is a simplified version of strings.ContainsRune // to avoid importing the strings package. // We avoid bytes.ContainsRune to avoid allocating a temporary copy of s. func containsRune(s string, r rune) bool { for _, c := range s { if c == r { return true } } return false } // Trim returns a subslice of s by slicing off all leading and // trailing UTF-8-encoded code points contained in cutset. func Trim(s []rune, cutset string) []rune { if len(s) == 0 { // This is what we've historically done. return nil } if cutset == "" { return s } return TrimLeft(TrimRight(s, cutset), cutset) } // TrimLeft returns a subslice of s by slicing off all leading // UTF-8-encoded code points contained in cutset. func TrimLeft(s []rune, cutset string) []rune { if len(s) == 0 { // This is what we've historically done. return nil } if cutset == "" { return s } for len(s) > 0 { r := s[0] if !containsRune(cutset, r) { break } s = s[1:] } if len(s) == 0 { // This is what we've historically done. return nil } return s } // TrimRight returns a subslice of s by slicing off all trailing // UTF-8-encoded code points that are contained in cutset. func TrimRight(s []rune, cutset string) []rune { if len(s) == 0 || cutset == "" { return s } for len(s) > 0 { r := s[len(s)-1] if !containsRune(cutset, r) { break } s = s[:len(s)-1] } return s } // TrimSpace returns a subslice of s by slicing off all leading and // trailing white space, as defined by Unicode. func TrimSpace(s []rune) []rune { return TrimFunc(s, unicode.IsSpace) } // TrimLeftFunc treats s as UTF-8-encoded bytes and returns a subslice of s by slicing off // all leading UTF-8-encoded code points c that satisfy f(c). func TrimLeftFunc(s []rune, f func(r rune) bool) []rune { i := indexFunc(s, f, false) if i == -1 { return nil } return s[i:] } // TrimRightFunc returns a subslice of s by slicing off all trailing // UTF-8-encoded code points c that satisfy f(c). func TrimRightFunc(s []rune, f func(r rune) bool) []rune { i := lastIndexFunc(s, f, false) return s[0 : i+1] } // TrimFunc returns a subslice of s by slicing off all leading and trailing // UTF-8-encoded code points c that satisfy f(c). func TrimFunc(s []rune, f func(r rune) bool) []rune { return TrimRightFunc(TrimLeftFunc(s, f), f) } // TrimPrefix returns s without the provided leading prefix string. // If s doesn't start with prefix, s is returned unchanged. func TrimPrefix(s, prefix []rune) []rune { if HasPrefix(s, prefix) { return s[len(prefix):] } return s } // TrimSuffix returns s without the provided trailing suffix string. // If s doesn't end with suffix, s is returned unchanged. func TrimSuffix(s, suffix []rune) []rune { if HasSuffix(s, suffix) { return s[:len(s)-len(suffix)] } return s } // Replace returns a copy of the slice s with the first n // non-overlapping instances of old replaced by new. // The old string cannot be empty. // If n < 0, there is no limit on the number of replacements. func Replace(s, old, new []rune, n int) []rune { if len(old) == 0 { panic("runes Replace: old cannot be empty") } m := 0 if n != 0 { // Compute number of replacements. m = Count(s, old) } if m == 0 { // Just return a copy. return append([]rune(nil), s...) } if n < 0 || m < n { n = m } // Apply replacements to buffer. t := make([]rune, len(s)+n*(len(new)-len(old))) w := 0 start := 0 for i := 0; i < n; i++ { j := start if len(old) == 0 { if i > 0 { j++ } } else { j += Index(s[start:], old) } w += copy(t[w:], s[start:j]) w += copy(t[w:], new) start = j + len(old) } w += copy(t[w:], s[start:]) return t[0:w] } // ReplaceAll returns a copy of the slice s with all // non-overlapping instances of old replaced by new. // If old is empty, it matches at the beginning of the slice // and after each UTF-8 sequence, yielding up to k+1 replacements // for a k-rune slice. func ReplaceAll(s, old, new []rune) []rune { return Replace(s, old, new, -1) } // 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 } // Generic split: splits after each instance of sep, // including sepSave bytes of sep in the subslices. func genSplit(s, sep []rune, sepSave, n int) [][]rune { if n == 0 { return nil } if len(sep) == 0 { panic("rune split: separator cannot be empty!") } if n < 0 { n = Count(s, sep) + 1 } if n > len(s)+1 { n = len(s) + 1 } a := make([][]rune, n) n-- i := 0 for i < n { m := Index(s, sep) if m < 0 { break } a[i] = s[: m+sepSave : m+sepSave] s = s[m+len(sep):] i++ } a[i] = s return a[:i+1] } // SplitN slices s into subslices separated by sep and returns a slice of // the subslices between those separators. // Sep cannot be empty. // The count determines the number of subslices to return: // // n > 0: at most n subslices; the last subslice will be the unsplit remainder. // n == 0: the result is nil (zero subslices) // n < 0: all subslices // // To split around the first instance of a separator, see Cut. func SplitN(s, sep []rune, n int) [][]rune { return genSplit(s, sep, 0, n) } // SplitAfterN slices s into subslices after each instance of sep and // returns a slice of those subslices. // If sep is empty, SplitAfterN splits after each UTF-8 sequence. // The count determines the number of subslices to return: // // n > 0: at most n subslices; the last subslice will be the unsplit remainder. // n == 0: the result is nil (zero subslices) // n < 0: all subslices func SplitAfterN(s, sep []rune, n int) [][]rune { return genSplit(s, sep, len(sep), n) } // Split slices s into all subslices separated by sep and returns a slice of // the subslices between those separators. // If sep is empty, Split splits after each UTF-8 sequence. // It is equivalent to SplitN with a count of -1. // // To split around the first instance of a separator, see Cut. func Split(s, sep []rune) [][]rune { return genSplit(s, sep, 0, -1) } // SplitAfter slices s into all subslices after each instance of sep and // returns a slice of those subslices. // If sep is empty, SplitAfter splits after each UTF-8 sequence. // It is equivalent to SplitAfterN with a count of -1. func SplitAfter(s, sep []rune) [][]rune { return genSplit(s, sep, len(sep), -1) } var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1} // Fields interprets s as a sequence of UTF-8-encoded code points. // It splits the slice s around each instance of one or more consecutive white space // characters, as defined by unicode.IsSpace, returning a slice of subslices of s or an // empty slice if s contains only white space. func Fields(s []rune) [][]rune { return FieldsFunc(s, unicode.IsSpace) } // FieldsFunc interprets s as a sequence of UTF-8-encoded code points. // It splits the slice s at each run of code points c satisfying f(c) and // returns a slice of subslices of s. If all code points in s satisfy f(c), or // len(s) == 0, an empty slice is returned. // // FieldsFunc makes no guarantees about the order in which it calls f(c) // and assumes that f always returns the same value for a given c. func FieldsFunc(s []rune, f func(rune) bool) [][]rune { // A span is used to record a slice of s of the form s[start:end]. // The start index is inclusive and the end index is exclusive. type span struct { start int end int } spans := make([]span, 0, 32) // Find the field start and end indices. // Doing this in a separate pass (rather than slicing the string s // and collecting the result substrings right away) is significantly // more efficient, possibly due to cache effects. start := -1 // valid span start if >= 0 for end, rune := range s { if f(rune) { if start >= 0 { spans = append(spans, span{start, end}) // Set start to a negative value. // Note: using -1 here consistently and reproducibly // slows down this code by a several percent on amd64. start = ^start } } else { if start < 0 { start = end } } } // Last field might end at EOF. if start >= 0 { spans = append(spans, span{start, len(s)}) } // Create strings from recorded field indices. a := make([][]rune, len(spans)) for i, span := range spans { a[i] = s[span.start:span.end:span.end] // last end makes it copy } return a } // Join concatenates the elements of s to create a new byte slice. The separator // sep is placed between elements in the resulting slice. func Join(s [][]rune, sep []rune) []rune { if len(s) == 0 { return []rune{} } if len(s) == 1 { // Just return a copy. return append([]rune(nil), s[0]...) } var n int if len(sep) > 0 { if len(sep) >= maxInt/(len(s)-1) { panic("bytes: Join output length overflow") } n += len(sep) * (len(s) - 1) } for _, v := range s { if len(v) > maxInt-n { panic("bytes: Join output length overflow") } n += len(v) } b := make([]rune, n) bp := copy(b, s[0]) for _, v := range s[1:] { bp += copy(b[bp:], sep) bp += copy(b[bp:], v) } return b } // HasPrefix reports whether the byte slice s begins with prefix. func HasPrefix(s, prefix []rune) bool { return len(s) >= len(prefix) && Equal(s[0:len(prefix)], prefix) } // HasSuffix reports whether the byte slice s ends with suffix. func HasSuffix(s, suffix []rune) bool { return len(s) >= len(suffix) && Equal(s[len(s)-len(suffix):], suffix) } // IndexFunc interprets s as a sequence of UTF-8-encoded code points. // It returns the byte index in s of the first Unicode // code point satisfying f(c), or -1 if none do. func IndexFunc(s []rune, f func(r rune) bool) int { return indexFunc(s, f, true) } // LastIndexFunc interprets s as a sequence of UTF-8-encoded code points. // It returns the byte index in s of the last Unicode // code point satisfying f(c), or -1 if none do. func LastIndexFunc(s []rune, f func(r rune) bool) int { return lastIndexFunc(s, f, true) } // indexFunc is the same as IndexFunc except that if // truth==false, the sense of the predicate function is // inverted. func indexFunc(s []rune, f func(r rune) bool, truth bool) int { for i, r := range s { if f(r) == truth { return i } } return -1 } // lastIndexFunc is the same as LastIndexFunc except that if // truth==false, the sense of the predicate function is // inverted. func lastIndexFunc(s []rune, f func(r rune) bool, truth bool) int { for i := len(s) - 1; i >= 0; i-- { if f(s[i]) == truth { return i } } return -1 } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package search import ( "errors" "io/fs" "path/filepath" "regexp" "sort" "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/core" "cogentcore.org/core/text/textpos" ) // All returns list of all files under given root path, in all subdirs, // of given language(s) that contain the given string, sorted in // descending order by number of occurrences. // - ignoreCase transforms everything into lowercase. // - regExp uses the go regexp syntax for the find string. // - exclude is a list of filenames to exclude. func All(root string, find string, ignoreCase, regExp bool, langs []fileinfo.Known, exclude ...string) ([]Results, error) { fsz := len(find) if fsz == 0 { return nil, nil } fb := []byte(find) var re *regexp.Regexp var err error if regExp { re, err = regexp.Compile(find) if err != nil { return nil, err } } mls := make([]Results, 0) var errs []error filepath.Walk(root, func(fpath string, info fs.FileInfo, err error) error { if err != nil { errs = append(errs, err) return err } if info.IsDir() { return nil } if int(info.Size()) > core.SystemSettings.BigFileSize { return nil } fname := info.Name() skip, err := excludeFile(&exclude, fname, fpath) if err != nil { errs = append(errs, err) } if skip { return nil } fi, err := fileinfo.NewFileInfo(fpath) if err != nil { errs = append(errs, err) } if fi.Generated { return nil } if !LangCheck(fi, langs) { return nil } var cnt int var matches []textpos.Match if regExp { cnt, matches = FileRegexp(fpath, re) } else { cnt, matches = File(fpath, fb, ignoreCase) } if cnt > 0 { fpabs, err := filepath.Abs(fpath) if err != nil { errs = append(errs, err) } mls = append(mls, Results{fpabs, cnt, matches}) } return nil }) sort.Slice(mls, func(i, j int) bool { return mls[i].Count > mls[j].Count }) return mls, errors.Join(errs...) } // 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 search import ( "bufio" "bytes" "fmt" "io" "log" "os" "regexp" "unicode/utf8" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/runes" "cogentcore.org/core/text/textpos" ) // Results is used to report search results. type Results struct { Filepath string Count int Matches []textpos.Match } func (r *Results) String() string { str := fmt.Sprintf("%s: %d", r.Filepath, r.Count) for _, m := range r.Matches { str += "\n" + m.String() } return str } // RuneLines 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 RuneLines(src [][]rune, find []byte, ignoreCase bool) (int, []textpos.Match) { fr := bytes.Runes(find) fsz := len(fr) if fsz == 0 { return 0, nil } cnt := 0 var matches []textpos.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 := textpos.NewMatch(rn, i, ci, ln) matches = append(matches, mat) cnt++ } } return cnt, matches } // LexItems 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 LexItems(src [][]rune, lexs []lexer.Line, find []byte, ignoreCase bool) (int, []textpos.Match) { fr := bytes.Runes(find) fsz := len(fr) if fsz == 0 { return 0, nil } cnt := 0 var matches []textpos.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.End - lx.Start if sz != fsz { continue } rn := rln[lx.Start:lx.End] var i int if ignoreCase { i = runes.IndexFold(rn, fr) } else { i = runes.Index(rn, fr) } if i < 0 { continue } mat := textpos.NewMatch(rln, lx.Start, lx.End, ln) matches = append(matches, mat) cnt++ } } return cnt, matches } // Reader looks for a literal 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 Reader(reader io.Reader, find []byte, ignoreCase bool) (int, []textpos.Match) { fr := bytes.Runes(find) fsz := len(fr) if fsz == 0 { return 0, nil } cnt := 0 var matches []textpos.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 := textpos.NewMatch(rn, i, ci, ln) matches = append(matches, mat) cnt++ } ln++ } return cnt, matches } // File looks for a literal string (no regexp) within a file, in given // case-sensitive way, returning number of occurrences and specific match // position list. Column positions are in runes. func File(filename string, find []byte, ignoreCase bool) (int, []textpos.Match) { fp, err := os.Open(filename) if err != nil { log.Printf("search.File: open error: %v\n", err) return 0, nil } defer fp.Close() return Reader(fp, find, ignoreCase) } // ReaderRegexp looks for a string using Go regexp expression, // from an io.Reader input stream. // Returns number of occurrences and specific match position list. // Column positions are in runes. func ReaderRegexp(reader io.Reader, re *regexp.Regexp) (int, []textpos.Match) { cnt := 0 var matches []textpos.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 := textpos.NewMatch(rn, ri[st], ri[ed], ln) matches = append(matches, mat) cnt++ } ln++ } return cnt, matches } // FileRegexp looks for a string using Go regexp expression // within a file, returning number of occurrences and specific match // position list. Column positions are in runes. func FileRegexp(filename string, re *regexp.Regexp) (int, []textpos.Match) { fp, err := os.Open(filename) if err != nil { log.Printf("search.FileRegexp: open error: %v\n", err) return 0, nil } defer fp.Close() return ReaderRegexp(fp, re) } // RuneLinesRegexp looks for a regexp within lines of runes, // with given case-sensitivity returning number of occurrences // and specific match position list. Column positions are in runes. func RuneLinesRegexp(src [][]rune, re *regexp.Regexp) (int, []textpos.Match) { cnt := 0 var matches []textpos.Match for ln := range src { // note: insane that we have to convert back and forth from bytes! b := []byte(string(src[ln])) 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 := textpos.NewMatch(rn, ri[st], ri[ed], ln) matches = append(matches, mat) cnt++ } } return cnt, matches } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package search import ( "errors" "os" "path/filepath" "regexp" "slices" "sort" "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/core" "cogentcore.org/core/text/textpos" ) // excludeFile does the exclude match against either file name or file path, // removes any problematic exclude expressions from the list. func excludeFile(exclude *[]string, fname, fpath string) (bool, error) { var errs []error for ei, ex := range *exclude { exp, err := filepath.Match(ex, fpath) if err != nil { errs = append(errs, err) *exclude = slices.Delete(*exclude, ei, ei+1) } if exp { return true, errors.Join(errs...) } exf, _ := filepath.Match(ex, fname) if exf { return true, errors.Join(errs...) } } return false, errors.Join(errs...) } // LangCheck checks if file matches list of target languages: true if // matches (or no langs) func LangCheck(fi *fileinfo.FileInfo, langs []fileinfo.Known) bool { if len(langs) == 0 { return true } if fileinfo.IsMatchList(langs, fi.Known) { return true } return false } // Paths returns list of all files in given list of paths (only: no subdirs), // of language(s) that contain the given string, sorted in descending order // by number of occurrences. Paths can be relative to current working directory. // Automatically skips generated files. // - ignoreCase transforms everything into lowercase. // - regExp uses the go regexp syntax for the find string. // - exclude is a list of filenames to exclude: can use standard Glob patterns. func Paths(paths []string, find string, ignoreCase, regExp bool, langs []fileinfo.Known, exclude ...string) ([]Results, error) { fsz := len(find) if fsz == 0 { return nil, nil } fb := []byte(find) var re *regexp.Regexp var err error if regExp { re, err = regexp.Compile(find) if err != nil { return nil, err } } mls := make([]Results, 0) var errs []error for _, path := range paths { files, err := os.ReadDir(path) if err != nil { errs = append(errs, err) continue } for _, de := range files { if de.IsDir() { continue } fname := de.Name() fpath := filepath.Join(path, fname) skip, err := excludeFile(&exclude, fname, fpath) if err != nil { errs = append(errs, err) } if skip { continue } fi, err := fileinfo.NewFileInfo(fpath) if err != nil { errs = append(errs, err) } if int(fi.Size) > core.SystemSettings.BigFileSize { continue } if fi.Generated { continue } if !LangCheck(fi, langs) { continue } var cnt int var matches []textpos.Match if regExp { cnt, matches = FileRegexp(fpath, re) } else { cnt, matches = File(fpath, fb, ignoreCase) } if cnt > 0 { fpabs, err := filepath.Abs(fpath) if err != nil { errs = append(errs, err) } mls = append(mls, Results{fpabs, cnt, matches}) } } } sort.Slice(mls, func(i, j int) bool { return mls[i].Count > mls[j].Count }) return mls, errors.Join(errs...) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package shaped import ( "fmt" "image" "image/color" "cogentcore.org/core/math32" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" "cogentcore.org/core/text/textpos" "golang.org/x/image/math/fixed" ) // todo: split source at para boundaries and use wrap para on those. // Lines is a list of Lines of shaped text, with an overall bounding // box and position for the entire collection. This is the renderable // unit of text, although it is not a [render.Item] because it lacks // a position, and it can potentially be re-used in different positions. type Lines struct { // Source is the original input source that generated this set of lines. // Each Line has its own set of spans that describes the Line contents. Source rich.Text // Lines are the shaped lines. Lines []Line // Offset is an optional offset to add to the position given when rendering. Offset math32.Vector2 // Bounds is the bounding box for the entire set of rendered text, // relative to a rendering Position (and excluding any contribution // of Offset). Use Size() method to get the size and ToRect() to get // an [image.Rectangle]. Bounds math32.Box2 // FontSize is the [rich.Context] StandardSize from the Context used // at the time of shaping. Actual lines can be larger depending on font // styling parameters. FontSize float32 // LineHeight is the line height used at the time of shaping. LineHeight float32 // Truncated indicates whether any lines were truncated. Truncated bool // Direction is the default text rendering direction from the Context. Direction rich.Directions // Links holds any hyperlinks within shaped text. Links []rich.Hyperlink // Color is the default fill color to use for inking text. Color color.Color // SelectionColor is the color to use for rendering selected regions. SelectionColor image.Image // HighlightColor is the color to use for rendering highlighted regions. HighlightColor image.Image } // Line is one line of shaped text, containing multiple Runs. // This is not an independent render target: see [Lines] (can always // use one Line per Lines as needed). type Line struct { // Source is the input source corresponding to the line contents, // derived from the original Lines Source. The style information for // each Run is embedded here. Source rich.Text // SourceRange is the range of runes in the original [Lines.Source] that // are represented in this line. SourceRange textpos.Range // Runs are the shaped [Run] elements. Runs []Run // Offset specifies the relative offset from the Lines Position // determining where to render the line in a target render image. // This is the baseline position (not the upper left: see Bounds for that). Offset math32.Vector2 // Bounds is the bounding box for the Line of rendered text, // relative to the baseline rendering position (excluding any contribution // of Offset). This is centered at the baseline and the upper left // typically has a negative Y. Use Size() method to get the size // and ToRect() to get an [image.Rectangle]. This is based on the output // LineBounds, not the actual GlyphBounds. Bounds math32.Box2 // Selections specifies region(s) of runes within this line that are selected, // and will be rendered with the [Lines.SelectionColor] background, // replacing any other background color that might have been specified. Selections []textpos.Range // Highlights specifies region(s) of runes within this line that are highlighted, // and will be rendered with the [Lines.HighlightColor] background, // replacing any other background color that might have been specified. Highlights []textpos.Range } func (ln *Line) String() string { return ln.Source.String() + fmt.Sprintf(" runs: %d\n", len(ln.Runs)) } func (ls *Lines) String() string { str := "" for li := range ls.Lines { ln := &ls.Lines[li] str += fmt.Sprintf("#### Line: %d\n", li) str += ln.String() } return str } // StartAtBaseline removes the offset from the first line that causes // the lines to be rendered starting at the upper left corner, so they // will instead be rendered starting at the baseline position. func (ls *Lines) StartAtBaseline() { if len(ls.Lines) == 0 { return } ls.Lines[0].Offset = math32.Vector2{} } // SetGlyphXAdvance sets the x advance on all glyphs to given value: // for monospaced case. func (ls *Lines) SetGlyphXAdvance(adv fixed.Int26_6) { for li := range ls.Lines { ln := &ls.Lines[li] for ri := range ln.Runs { rn := ln.Runs[ri] rn.SetGlyphXAdvance(adv) } } } // GetLinks gets the links for these lines, which are cached in Links. func (ls *Lines) GetLinks() []rich.Hyperlink { if ls.Links != nil { return ls.Links } ls.Links = ls.Source.GetLinks() return ls.Links } // AlignXFactor aligns the lines along X axis according to alignment factor, // as a proportion of size difference to add to offset (0.5 = center, // 1 = right) func (ls *Lines) AlignXFactor(fact float32) { wd := ls.Bounds.Size().X for li := range ls.Lines { ln := &ls.Lines[li] lwd := ln.Bounds.Size().X if lwd < wd { ln.Offset.X += fact * (wd - lwd) } } } // AlignX aligns the lines along X axis according to text style. func (ls *Lines) AlignX(tsty *text.Style) { fact, _ := tsty.AlignFactors() if fact > 0 { ls.AlignXFactor(fact) } } // Clone returns a Clone copy of the Lines, with new Lines elements // that still point to the same underlying Runs. func (ls *Lines) Clone() *Lines { nls := &Lines{} *nls = *ls nln := len(ls.Lines) if nln > 0 { nln := make([]Line, nln) for i := range ls.Lines { nln[i] = ls.Lines[i] } nls.Lines = nln } return nls } // UpdateStyle updates the Decoration, Fill and Stroke colors from the given // rich.Text Styles for each line and given text style. // This rich.Text must match the content of the shaped one, and differ only // in these non-layout styles. func (ls *Lines) UpdateStyle(tx rich.Text, tsty *text.Style) { ls.Source = tx ls.Color = tsty.Color for i := range ls.Lines { ln := &ls.Lines[i] ln.UpdateStyle(tx, tsty) } } // UpdateStyle updates the Decoration, Fill and Stroke colors from the current // rich.Text Style for each run and given text style. // This rich.Text must match the content of the shaped one, and differ only // in these non-layout styles. func (ln *Line) UpdateStyle(tx rich.Text, tsty *text.Style) { ln.Source = tx for ri, rn := range ln.Runs { fs := ln.RunStyle(ln.Source, ri) rb := rn.AsBase() rb.SetFromStyle(fs, tsty) } } // RunStyle returns the rich text style for given run index. func (ln *Line) RunStyle(tx rich.Text, ri int) *rich.Style { rn := ln.Runs[ri] rs := rn.Runes().Start si, _, _ := tx.Index(rs) sty, _ := tx.Span(si) return sty } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package shaped import ( "cogentcore.org/core/math32" "cogentcore.org/core/text/textpos" ) // SelectRegion adds the selection to given region of runes from // the original source runes. Use SelectReset to clear first if desired. func (ls *Lines) SelectRegion(r textpos.Range) { nr := ls.Source.Len() r = r.Intersect(textpos.Range{0, nr}) for li := range ls.Lines { ln := &ls.Lines[li] lr := r.Intersect(ln.SourceRange) if lr.Len() > 0 { ln.Selections = append(ln.Selections, lr) } } } // SelectReset removes all existing selected regions. func (ls *Lines) SelectReset() { for li := range ls.Lines { ln := &ls.Lines[li] ln.Selections = nil } } // HighlightRegion adds the selection to given region of runes from // the original source runes. Use HighlightReset to clear first if desired. func (ls *Lines) HighlightRegion(r textpos.Range) { nr := ls.Source.Len() r = r.Intersect(textpos.Range{0, nr}) for li := range ls.Lines { ln := &ls.Lines[li] lr := r.Intersect(ln.SourceRange) if lr.Len() > 0 { ln.Highlights = append(ln.Highlights, lr) } } } // HighlightReset removes all existing selected regions. func (ls *Lines) HighlightReset() { for li := range ls.Lines { ln := &ls.Lines[li] ln.Highlights = nil } } // RuneToLinePos returns the [textpos.Pos] line and character position for given rune // index in Lines source. If ti >= source Len(), returns a position just after // the last actual rune. func (ls *Lines) RuneToLinePos(ti int) textpos.Pos { if len(ls.Lines) == 0 { return textpos.Pos{} } n := ls.Source.Len() el := len(ls.Lines) - 1 ep := textpos.Pos{el, ls.Lines[el].SourceRange.End} if ti >= n { return ep } for li := range ls.Lines { ln := &ls.Lines[li] if !ln.SourceRange.Contains(ti) { continue } return textpos.Pos{li, ti - ln.SourceRange.Start} } return ep // shouldn't happen } // RuneFromLinePos returns the rune index in Lines source for given // [textpos.Pos] line and character position. Returns Len() of source // if it goes past that. func (ls *Lines) RuneFromLinePos(tp textpos.Pos) int { if len(ls.Lines) == 0 { return 0 } n := ls.Source.Len() nl := len(ls.Lines) if tp.Line >= nl { return n } ln := &ls.Lines[tp.Line] return ln.SourceRange.Start + tp.Char } // RuneAtLineDelta returns the rune index in Lines source at given // relative vertical offset in lines from the current line for given rune. // It uses pixel locations of glyphs and the LineHeight to find the // rune at given vertical offset with the same horizontal position. // If the delta goes out of range, it will return the appropriate in-range // rune index at the closest horizontal position. func (ls *Lines) RuneAtLineDelta(ti, lineDelta int) int { rp := ls.RuneBounds(ti).Center() tp := rp ld := float32(lineDelta) * ls.LineHeight // todo: should iterate over lines for different sizes.. tp.Y = math32.Clamp(tp.Y+ld, ls.Bounds.Min.Y+2, ls.Bounds.Max.Y-2) return ls.RuneAtPoint(tp, math32.Vector2{}) } // RuneBounds returns the glyph bounds for given rune index in Lines source, // relative to the upper-left corner of the lines bounding box. // If the index is >= the source length, it returns a box at the end of the // rendered text (i.e., where a cursor should be to add more text). func (ls *Lines) RuneBounds(ti int) math32.Box2 { n := ls.Source.Len() zb := math32.Box2{} if len(ls.Lines) == 0 { return zb } start := ls.Offset if ti >= n { // goto end ln := ls.Lines[len(ls.Lines)-1] off := start.Add(ln.Offset) ep := ln.Bounds.Max.Add(off) ep.Y = ln.Bounds.Min.Y + off.Y return math32.Box2{ep, ep} } for li := range ls.Lines { ln := &ls.Lines[li] if !ln.SourceRange.Contains(ti) { continue } off := start.Add(ln.Offset) for ri := range ln.Runs { run := ln.Runs[ri] rr := run.Runes() if ti >= rr.End { off.X += run.Advance() continue } bb := run.RuneBounds(ti) return bb.Translate(off) } } return zb } // RuneAtPoint returns the rune index in Lines source, at given rendered location, // based on given starting location for rendering. If the point is out of the // line bounds, the nearest point is returned (e.g., start of line based on Y coordinate). func (ls *Lines) RuneAtPoint(pt math32.Vector2, start math32.Vector2) int { start.SetAdd(ls.Offset) lbb := ls.Bounds.Translate(start) if !lbb.ContainsPoint(pt) { // smaller bb so point will be inside stuff sbb := math32.Box2{lbb.Min.Add(math32.Vec2(0, 2)), lbb.Max.Sub(math32.Vec2(0, 2))} pt = sbb.ClampPoint(pt) } nl := len(ls.Lines) for li := range ls.Lines { ln := &ls.Lines[li] off := start.Add(ln.Offset) lbb := ln.Bounds.Translate(off) if !lbb.ContainsPoint(pt) { if pt.Y >= lbb.Min.Y && pt.Y < lbb.Max.Y { // this is our line if pt.X <= lbb.Min.X { return ln.SourceRange.Start } return ln.SourceRange.End } continue } for ri := range ln.Runs { run := ln.Runs[ri] rbb := run.AsBase().MaxBounds.Translate(off) if !rbb.ContainsPoint(pt) { off.X += run.Advance() continue } rp := run.RuneAtPoint(ls.Source, pt, off) if rp == run.Runes().End && li < nl-1 { // if not at full end, don't go past rp-- } return rp } return ln.SourceRange.End } return 0 } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package shaped import ( "image" "cogentcore.org/core/colors" "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" "cogentcore.org/core/text/textpos" "golang.org/x/image/math/fixed" ) // Run is a span of shaped text with the same font properties, // with layout information to enable GUI interaction with shaped text. type Run interface { // AsBase returns the base type with relevant shaped text information. AsBase() *RunBase // LineBounds returns the Line-level Bounds for given Run as rect bounding box. LineBounds() math32.Box2 // Runes returns our rune range in original source using textpos.Range. Runes() textpos.Range // Advance returns the total distance to advance in going from one run to the next. Advance() float32 // RuneBounds returns the maximal line-bounds level bounding box for given rune index. RuneBounds(ri int) math32.Box2 // RuneAtPoint returns the rune index in Lines source, at given rendered location, // based on given starting location for rendering. If the point is out of the // line bounds, the nearest point is returned (e.g., start of line based on Y coordinate). RuneAtPoint(src rich.Text, pt math32.Vector2, start math32.Vector2) int // SetGlyphXAdvance sets the x advance on all glyphs to given value: // for monospaced case. SetGlyphXAdvance(adv fixed.Int26_6) } // Math holds the output of a TeX math expression. type Math struct { Path *ppath.Path BBox math32.Box2 } // Run is a span of text with the same font properties, with full rendering information. type RunBase struct { // Font is the [text.Font] compact encoding of the font to use for rendering. Font text.Font // MaxBounds are the maximal line-level bounds for this run, suitable for region // rendering and mouse interaction detection. MaxBounds math32.Box2 // Decoration are the decorations from the style to apply to this run. Decoration rich.Decorations // Math holds the output of Math formatting via the tex package. Math Math // FillColor is the color to use for glyph fill (i.e., the standard "ink" color). // Will only be non-nil if set for this run; Otherwise use default. FillColor image.Image // StrokeColor is the color to use for glyph outline stroking, if non-nil. StrokeColor image.Image // Background is the color to use for the background region, if non-nil. Background image.Image } // SetFromStyle sets the run styling parameters from given styles. // Will also update non-Font elements, but font can only be set first time // in the initial shaping process, otherwise the render is off. func (run *RunBase) SetFromStyle(sty *rich.Style, tsty *text.Style) { run.Decoration = sty.Decoration if run.Font.Size == 0 { run.Font = *text.NewFont(sty, tsty) } if sty.Decoration.HasFlag(rich.FillColor) { run.FillColor = colors.Uniform(sty.FillColor()) } else { run.FillColor = nil } if sty.Decoration.HasFlag(rich.StrokeColor) { run.StrokeColor = colors.Uniform(sty.StrokeColor()) } else { run.StrokeColor = nil } if sty.Decoration.HasFlag(rich.Background) { run.Background = colors.Uniform(sty.Background()) } else { run.Background = nil } } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package shaped import ( "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" "cogentcore.org/core/text/fonts" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/text" "github.com/go-text/typesetting/di" ) //go:generate core generate var ( // NewShaper returns the correct type of shaper. NewShaper func() Shaper // ShapeMath is a function that returns a path representing the // given math expression, in TeX syntax. // Import _ cogentcore.org/core/text/tex to set this function // (incurs a significant additional memory footprint due to fonts // and other packages). ShapeMath func(expr string, fontHeight float32) (*ppath.Path, error) ) // Shaper is a text shaping system that can shape the layout of [rich.Text], // including line wrapping. All functions are protected by a mutex. type Shaper interface { // Shape turns given input spans into [Runs] of rendered text, // using given context needed for complete styling. // The results are only valid until the next call to Shape or WrapParagraph: // use slices.Clone if needed longer than that. // This is called under a mutex lock, so it is safe for parallel use. Shape(tx rich.Text, tsty *text.Style, rts *rich.Settings) []Run // WrapLines performs line wrapping and shaping on the given rich text source, // using the given style information, where the [rich.Style] provides the default // style information reflecting the contents of the source (e.g., the default family, // weight, etc), for use in computing the default line height. Paragraphs are extracted // first using standard newline markers, assumed to coincide with separate spans in the // source text, and wrapped separately. For horizontal text, the Lines will render with // a position offset at the upper left corner of the overall bounding box of the text. // This is called under a mutex lock, so it is safe for parallel use. WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style, rts *rich.Settings, size math32.Vector2) *Lines // FontFamilies returns a list of available font family names on this system. FontList() []fonts.Info } // WrapSizeEstimate 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 WrapSizeEstimate(csz math32.Vector2, nChars int, ratio float32, sty *rich.Style, tsty *text.Style) math32.Vector2 { chars := float32(nChars) fht := tsty.FontHeight(sty) if fht == 0 { fht = 16 } area := chars * fht * fht if csz.X > 0 && csz.Y > 0 { ratio = csz.X / csz.Y } // w = ratio * h // w * h = a // h^2 = a / r // h = sqrt(a / r) h := math32.Sqrt(area / ratio) h = max(fht*math32.Floor(h/fht), fht) w := area / 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 } // GoTextDirection gets the proper go-text direction value from styles. func GoTextDirection(rdir rich.Directions, tsty *text.Style) di.Direction { dir := tsty.Direction if rdir != rich.Default { dir = rdir } return dir.ToGoText() } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package shapedgt import ( "os" "cogentcore.org/core/base/errors" "cogentcore.org/core/text/fonts" "cogentcore.org/core/text/rich" "github.com/go-text/typesetting/fontscan" ) // FontList returns the list of fonts that have been loaded. func (sh *Shaper) FontList() []fonts.Info { str := errors.Log1(os.UserCacheDir()) ft := errors.Log1(fontscan.SystemFonts(nil, str)) fi := make([]fonts.Info, len(ft)) for i := range ft { fi[i].Family = ft[i].Family as := ft[i].Aspect fi[i].Weight = rich.Weights(int(as.Weight / 100.0)) fi[i].Slant = rich.Slants(as.Style - 1) // fi[i].Stretch = rich.Stretch() // not avail fi[i].Stretch = rich.StretchNormal fi[i].Font = ft[i] } return fi } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package shapedgt import ( "fmt" "cogentcore.org/core/math32" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/textpos" "github.com/go-text/typesetting/shaping" "golang.org/x/image/math/fixed" ) // Run is a span of text with the same font properties, with full rendering information. type Run struct { shaped.RunBase shaping.Output } func (run *Run) AsBase() *shaped.RunBase { return &run.RunBase } func (run *Run) Advance() float32 { return math32.FromFixed(run.Output.Advance) } // Runes returns our rune range using textpos.Range func (run *Run) Runes() textpos.Range { return textpos.Range{run.Output.Runes.Offset, run.Output.Runes.Offset + run.Output.Runes.Count} } // GlyphBoundsBox returns the math32.Box2 version of [Run.GlyphBounds], // providing a tight bounding box for given glyph within this run. func (run *Run) GlyphBoundsBox(g *shaping.Glyph) math32.Box2 { if run.Math.Path != nil { return run.MaxBounds } return math32.B2FromFixed(run.GlyphBounds(g)) } // GlyphBounds returns the tight bounding box for given glyph within this run. func (run *Run) GlyphBounds(g *shaping.Glyph) fixed.Rectangle26_6 { if run.Math.Path != nil { return run.MaxBounds.ToFixed() } if run.Direction.IsVertical() { if run.Direction.IsSideways() { fmt.Println("sideways") return fixed.Rectangle26_6{Min: fixed.Point26_6{X: g.XBearing, Y: -g.YBearing}, Max: fixed.Point26_6{X: g.XBearing + g.Width, Y: -g.YBearing - g.Height}} } return fixed.Rectangle26_6{Min: fixed.Point26_6{X: -g.XBearing - g.Width/2, Y: g.Height - g.YOffset}, Max: fixed.Point26_6{X: g.XBearing + g.Width/2, Y: -(g.YBearing + g.Height) - g.YOffset}} } return fixed.Rectangle26_6{Min: fixed.Point26_6{X: g.XBearing, Y: -g.YBearing}, Max: fixed.Point26_6{X: g.XBearing + g.Width, Y: -g.YBearing - g.Height}} } // GlyphLineBoundsBox returns the math32.Box2 version of [Run.GlyphLineBounds], // providing a line-level bounding box for given glyph within this run. func (run *Run) GlyphLineBoundsBox(g *shaping.Glyph) math32.Box2 { if run.Math.Path != nil { return run.MaxBounds } return math32.B2FromFixed(run.GlyphLineBounds(g)) } // GlyphLineBounds returns the line-level bounding box for given glyph within this run. func (run *Run) GlyphLineBounds(g *shaping.Glyph) fixed.Rectangle26_6 { if run.Math.Path != nil { return run.MaxBounds.ToFixed() } rb := run.Bounds() if run.Direction.IsVertical() { // todo: fixme if run.Direction.IsSideways() { fmt.Println("sideways") return fixed.Rectangle26_6{Min: fixed.Point26_6{X: g.XBearing, Y: -g.YBearing}, Max: fixed.Point26_6{X: g.XBearing + g.Width, Y: -g.YBearing - g.Height}} } return fixed.Rectangle26_6{Min: fixed.Point26_6{X: -g.XBearing - g.Width/2, Y: g.Height - g.YOffset}, Max: fixed.Point26_6{X: g.XBearing + g.Width/2, Y: -(g.YBearing + g.Height) - g.YOffset}} } return fixed.Rectangle26_6{Min: fixed.Point26_6{X: g.XBearing, Y: rb.Min.Y}, Max: fixed.Point26_6{X: g.XBearing + g.Width, Y: rb.Max.Y}} } // LineBounds returns the LineBounds for given Run as a math32.Box2 // bounding box func (run *Run) LineBounds() math32.Box2 { if run.Math.Path != nil { return run.MaxBounds } return math32.B2FromFixed(run.Bounds()) } // Bounds returns the LineBounds for given Run as rect bounding box. // See [Run.BoundsBox] for a version returning the float32 [math32.Box2]. func (run *Run) Bounds() fixed.Rectangle26_6 { if run.Math.Path != nil { return run.MaxBounds.ToFixed() } mb := run.MaxBounds if run.Direction.IsVertical() { // ascent, descent describe horizontal, advance is vertical // return fixed.Rectangle26_6{Min: fixed.Point26_6{X: -lb.Ascent, Y: 0}, // Max: fixed.Point26_6{X: -gapdec, Y: -run.Output.Advance}} } return fixed.Rectangle26_6{Min: fixed.Point26_6{X: 0, Y: mb.Min.ToFixed().Y}, Max: fixed.Point26_6{X: run.Output.Advance, Y: mb.Max.ToFixed().Y}} } // RunBounds returns the Advance-based Bounds for this Run as rect bounding box, // that reflects the total space of the run, using Ascent & Descent for font // for the vertical dimension in horizontal text. func (run *Run) RunBounds() fixed.Rectangle26_6 { if run.Math.Path != nil { return run.MaxBounds.ToFixed() } lb := &run.Output.LineBounds if run.Direction.IsVertical() { // ascent, descent describe horizontal, advance is vertical return fixed.Rectangle26_6{Min: fixed.Point26_6{X: -lb.Ascent, Y: 0}, Max: fixed.Point26_6{X: -lb.Descent, Y: -run.Output.Advance}} } return fixed.Rectangle26_6{Min: fixed.Point26_6{X: 0, Y: -lb.Ascent}, Max: fixed.Point26_6{X: run.Output.Advance, Y: -lb.Descent}} } // GlyphsAt returns the indexs of the glyph(s) at given original source rune index. // Empty if none found. func (run *Run) GlyphsAt(i int) []int { var gis []int for gi := range run.Glyphs { g := &run.Glyphs[gi] if g.ClusterIndex > i { break } if g.ClusterIndex == i { gis = append(gis, gi) } } return gis } // FirstGlyphAt returns the index of the first glyph at or above given original // source rune index, returns -1 if none found. func (run *Run) FirstGlyphAt(i int) int { for gi := range run.Glyphs { g := &run.Glyphs[gi] if g.ClusterIndex >= i { return gi } } return -1 } // LastGlyphAt returns the index of the last glyph at given original source rune index, // returns -1 if none found. func (run *Run) LastGlyphAt(i int) int { ng := len(run.Glyphs) for gi := ng - 1; gi >= 0; gi-- { g := &run.Glyphs[gi] if g.ClusterIndex <= i { return gi } } return -1 } // SetGlyphXAdvance sets the x advance on all glyphs to given value: // for monospaced case. func (run *Run) SetGlyphXAdvance(adv fixed.Int26_6) { for gi := range run.Glyphs { g := &run.Glyphs[gi] g.XAdvance = adv } run.Output.Advance = adv * fixed.Int26_6(len(run.Glyphs)) } // RuneAtPoint returns the index of the rune in the source, which contains given point, // using the maximal glyph bounding box. Off is the offset for this run within overall // image rendering context of point coordinates. Assumes point is already identified // as being within the [Run.MaxBounds]. func (run *Run) RuneAtPoint(src rich.Text, pt, off math32.Vector2) int { // todo: vertical case! adv := off.X rr := run.Runes() for gi := range run.Glyphs { g := &run.Glyphs[gi] cri := g.ClusterIndex gadv := math32.FromFixed(g.XAdvance) mx := adv + gadv // fmt.Println(gi, cri, adv, mx, pt.X) if pt.X >= adv && pt.X < mx { // fmt.Println("fits!") return cri } adv += gadv } return rr.End } // RuneBounds returns the maximal line-bounds level bounding box for given rune index. func (run *Run) RuneBounds(ri int) math32.Box2 { gis := run.GlyphsAt(ri) if len(gis) == 0 { fmt.Println("no glyphs") return (math32.Box2{}) } return run.GlyphRegionBounds(gis[0], gis[len(gis)-1]) } // GlyphRegionBounds returns the maximal line-bounds level bounding box // between two glyphs in this run, where the st comes before the ed. func (run *Run) GlyphRegionBounds(st, ed int) math32.Box2 { if run.Direction.IsVertical() { // todo: write me! return math32.Box2{} } sg := &run.Glyphs[st] stb := run.GlyphLineBoundsBox(sg) mb := run.MaxBounds off := float32(0) for gi := 0; gi < st; gi++ { g := &run.Glyphs[gi] off += math32.FromFixed(g.XAdvance) } mb.Min.X = off + stb.Min.X - 2 for gi := st; gi <= ed; gi++ { g := &run.Glyphs[gi] gb := run.GlyphBoundsBox(g) mb.Max.X = off + gb.Max.X + 2 off += math32.FromFixed(g.XAdvance) } return mb } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package shapedgt import ( "fmt" "io/fs" "os" "sync" "cogentcore.org/core/base/errors" "cogentcore.org/core/math32" "cogentcore.org/core/text/fonts" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/text" "github.com/go-text/typesetting/di" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/fontscan" "github.com/go-text/typesetting/shaping" "golang.org/x/image/math/fixed" ) // Shaper is the text shaper and wrapper, from go-text/shaping. type Shaper struct { shaper shaping.HarfbuzzShaper wrapper shaping.LineWrapper fontMap *fontscan.FontMap splitter shaping.Segmenter maths map[int]*shaped.Math // outBuff is the output buffer to avoid excessive memory consumption. outBuff []shaping.Output sync.Mutex } type nilLogger struct{} func (nl *nilLogger) Printf(format string, args ...any) {} // NewShaper returns a new text shaper. func NewShaper() shaped.Shaper { sh := &Shaper{} sh.fontMap = fontscan.NewFontMap(&nilLogger{}) // TODO(text): figure out cache dir situation (especially on mobile and web) str, err := os.UserCacheDir() if errors.Log(err) != nil { // slog.Printf("failed resolving font cache dir: %v", err) // shaper.logger.Printf("skipping system font load") } // fmt.Println("cache dir:", str) if err := sh.fontMap.UseSystemFonts(str); err != nil { // note: we expect this error on js platform -- could do something exclusive here // under a separate build tag file.. // errors.Log(err) // shaper.logger.Printf("failed loading system fonts: %v", err) } errors.Log(fonts.UseEmbeddedInMap(sh.fontMap)) sh.shaper.SetFontCacheSize(32) return sh } // NewShaperWithFonts returns a new text shaper using // given filesystem with fonts. func NewShaperWithFonts(fss []fs.FS) shaped.Shaper { sh := &Shaper{} sh.fontMap = fontscan.NewFontMap(&nilLogger{}) errors.Log(fonts.UseInMap(sh.fontMap, fss)) sh.shaper.SetFontCacheSize(32) return sh } // FontMap returns the font map used for this shaper func (sh *Shaper) FontMap() *fontscan.FontMap { return sh.fontMap } // Shape turns given input spans into [Runs] of rendered text, // using given context needed for complete styling. // The results are only valid until the next call to Shape or WrapParagraph: // use slices.Clone if needed longer than that. // This is called under a mutex lock, so it is safe for parallel use. func (sh *Shaper) Shape(tx rich.Text, tsty *text.Style, rts *rich.Settings) []shaped.Run { sh.Lock() defer sh.Unlock() return sh.ShapeText(tx, tsty, rts, tx.Join()) } // ShapeText shapes the spans in the given text using given style and settings, // returning [shaped.Run] results. // This should already have the mutex lock, and is used by shapedjs but is // not an end-user call. func (sh *Shaper) ShapeText(tx rich.Text, tsty *text.Style, rts *rich.Settings, txt []rune) []shaped.Run { outs := sh.ShapeTextOutput(tx, tsty, rts, txt) runs := make([]shaped.Run, len(outs)) for i := range outs { run := &Run{Output: outs[i]} si, _, _ := tx.Index(run.Runes().Start) sty, _ := tx.Span(si) run.SetFromStyle(sty, tsty) if sty.IsMath() { mt := sh.maths[si] if mt != nil { run.Math = *mt run.MaxBounds = mt.BBox run.Output.Advance = math32.ToFixed(mt.BBox.Size().X) } } runs[i] = run } return runs } // ShapeTextOutput shapes the spans in the given text using given style and settings, // returning raw go-text [shaping.Output]. // This should already have the mutex lock, and is used by shapedjs but is // not an end-user call. func (sh *Shaper) ShapeTextOutput(tx rich.Text, tsty *text.Style, rts *rich.Settings, txt []rune) []shaping.Output { if tx.Len() == 0 { return nil } sh.shapeMaths(tx, tsty) sty := rich.NewStyle() sh.outBuff = sh.outBuff[:0] for si, s := range tx { in := shaping.Input{} start, end := tx.Range(si) stx := sty.FromRunes(s) // sets sty, returns runes for span if len(stx) == 0 { continue } if sty.IsMath() { mt := sh.maths[si] o := shaping.Output{} o.Runes.Offset = start o.Runes.Count = end - start if mt != nil { o.Advance = math32.ToFixed(mt.BBox.Size().X) } sh.outBuff = append(sh.outBuff, o) si++ // skip the end special continue } q := StyleToQuery(sty, tsty, rts) sh.fontMap.SetQuery(q) in.Text = txt in.RunStart = start in.RunEnd = end in.Direction = shaped.GoTextDirection(sty.Direction, tsty) fsz := tsty.FontHeight(sty) in.Size = math32.ToFixed(fsz) in.Script = rts.Script in.Language = rts.Language ins := sh.splitter.Split(in, sh.fontMap) // this is essential for _, in := range ins { if in.Face == nil { fmt.Println("nil face in input", len(stx), string(stx)) // fmt.Printf("nil face for in: %#v\n", in) continue } o := sh.shaper.Shape(in) FixOutputZeros(&o) sh.outBuff = append(sh.outBuff, o) } } return sh.outBuff } // shapeMaths runs TeX on all Math specials, saving results in maths // map indexed by the span index. func (sh *Shaper) shapeMaths(tx rich.Text, tsty *text.Style) { sh.maths = make(map[int]*shaped.Math) if shaped.ShapeMath == nil { return } for si, _ := range tx { sty, stx := tx.Span(si) if sty.IsMath() { mt := sh.shapeMath(sty, tsty, stx) sh.maths[si] = mt // can be nil if error si++ // skip past special } } } // shapeMath runs tex math to get path for math special func (sh *Shaper) shapeMath(sty *rich.Style, tsty *text.Style, stx []rune) *shaped.Math { if shaped.ShapeMath == nil { return nil } mstr := string(stx) if sty.Special == rich.MathDisplay { mstr = "$" + mstr + "$" } p := errors.Log1(shaped.ShapeMath(mstr, tsty.FontHeight(sty))) if p != nil { bb := p.FastBounds() bb.Max.X += 5 // extra space return &shaped.Math{Path: p, BBox: bb} } return nil } // todo: do the paragraph splitting! write fun in rich.Text // DirectionAdvance advances given position based on given direction. func DirectionAdvance(dir di.Direction, pos fixed.Point26_6, adv fixed.Int26_6) fixed.Point26_6 { if dir.IsVertical() { pos.Y += -adv } else { pos.X += adv } return pos } // StyleToQuery translates the rich.Style to go-text fontscan.Query parameters. func StyleToQuery(sty *rich.Style, tsty *text.Style, rts *rich.Settings) fontscan.Query { q := fontscan.Query{} fam := tsty.FontFamily(sty) q.Families = rich.FamiliesToList(fam) q.Aspect = StyleToAspect(sty) return q } // StyleToAspect translates the rich.Style to go-text font.Aspect parameters. func StyleToAspect(sty *rich.Style) font.Aspect { as := font.Aspect{} as.Style = font.Style(1 + sty.Slant) as.Weight = font.Weight(sty.Weight.ToFloat32()) as.Stretch = font.Stretch(sty.Stretch.ToFloat32()) return as } // FixOutputZeros fixes zero values in output, which can happen with emojis. func FixOutputZeros(o *shaping.Output) { for gi := range o.Glyphs { g := &o.Glyphs[gi] if g.Width == 0 { // fmt.Println(gi, g.GlyphID, "fixed width:", g.XAdvance) g.Width = g.XAdvance } if g.Height == 0 { // fmt.Println(gi, "fixed height:", o.Size) g.Height = o.Size } } } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package shapedgt import ( "fmt" "cogentcore.org/core/math32" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/text" "github.com/go-text/typesetting/di" "github.com/go-text/typesetting/shaping" "golang.org/x/image/math/fixed" ) // WrapLines performs line wrapping and shaping on the given rich text source, // using the given style information, where the [rich.Style] provides the default // style information reflecting the contents of the source (e.g., the default family, // weight, etc), for use in computing the default line height. Paragraphs are extracted // first using standard newline markers, assumed to coincide with separate spans in the // source text, and wrapped separately. For horizontal text, the Lines will render with // a position offset at the upper left corner of the overall bounding box of the text. // This is called under a mutex lock, so it is safe for parallel use. func (sh *Shaper) WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style, rts *rich.Settings, size math32.Vector2) *shaped.Lines { sh.Lock() defer sh.Unlock() if tsty.FontSize.Dots == 0 { tsty.FontSize.Dots = 16 } txt := tx.Join() outs := sh.ShapeTextOutput(tx, tsty, rts, txt) lines, truncated := sh.WrapLinesOutput(outs, txt, tx, defSty, tsty, rts, size) return sh.LinesBounds(lines, truncated, tx, defSty, tsty, size) } // This should already have the mutex lock, and is used by shapedjs but is // not an end-user call. Returns new lines and number of truncations. func (sh *Shaper) WrapLinesOutput(outs []shaping.Output, txt []rune, tx rich.Text, defSty *rich.Style, tsty *text.Style, rts *rich.Settings, size math32.Vector2) ([]shaping.Line, int) { lht := tsty.LineHeightDots(defSty) dir := shaped.GoTextDirection(rich.Default, tsty) nlines := int(math32.Floor(size.Y/lht)) * 2 maxSize := int(size.X) if dir.IsVertical() { nlines = int(math32.Floor(size.X / lht)) maxSize = int(size.Y) // fmt.Println(lht, nlines, maxSize) } // fmt.Println("lht:", lns.LineHeight, lgap, nlines) brk := shaping.WhenNecessary switch tsty.WhiteSpace { case text.WrapNever: brk = shaping.Never case text.WhiteSpacePre: maxSize = 100000 case text.WrapAlways: brk = shaping.Always } if brk == shaping.Never { maxSize = 100000 nlines = 1 } // fmt.Println(brk, nlines, maxSize) cfg := shaping.WrapConfig{ Direction: dir, TruncateAfterLines: nlines, TextContinues: false, // todo! no effect if TruncateAfterLines is 0 BreakPolicy: brk, // or Never, Always DisableTrailingWhitespaceTrim: tsty.WhiteSpace.KeepWhiteSpace(), } // from gio: // if wc.TruncateAfterLines > 0 { // if len(params.Truncator) == 0 { // params.Truncator = "…" // } // // We only permit a single run as the truncator, regardless of whether more were generated. // // Just use the first one. // wc.Truncator = s.ShapeText(params.PxPerEm, params.Locale, []rune(params.Truncator))[0] // } // todo: WrapParagraph does NOT handle vertical text! file issue. return sh.wrapper.WrapParagraph(cfg, maxSize, txt, shaping.NewSliceIterator(outs)) } // This should already have the mutex lock, and is used by shapedjs but is // not an end-user call. func (sh *Shaper) LinesBounds(lines []shaping.Line, truncated int, tx rich.Text, defSty *rich.Style, tsty *text.Style, size math32.Vector2) *shaped.Lines { lht := tsty.LineHeightDots(defSty) lns := &shaped.Lines{Source: tx, Color: tsty.Color, SelectionColor: tsty.SelectColor, HighlightColor: tsty.HighlightColor, LineHeight: lht} lns.Truncated = truncated > 0 fsz := tsty.FontHeight(defSty) dir := shaped.GoTextDirection(rich.Default, tsty) // fmt.Println(fsz, lht, lht/fsz, tsty.LineHeight) cspi := 0 cspSt, cspEd := tx.Range(cspi) var off math32.Vector2 for li, lno := range lines { // fmt.Println("line:", li, off) ln := shaped.Line{} var lsp rich.Text var pos fixed.Point26_6 setFirst := false var maxAsc fixed.Int26_6 maxLHt := lht for oi := range lno { out := &lno[oi] FixOutputZeros(out) if !dir.IsVertical() { // todo: vertical maxAsc = max(out.LineBounds.Ascent, maxAsc) } run := Run{Output: *out} rns := run.Runes() if !setFirst { ln.SourceRange.Start = rns.Start setFirst = true } ln.SourceRange.End = rns.End for rns.Start >= cspEd { cspi++ cspSt, cspEd = tx.Range(cspi) } sty, cr := rich.NewStyleFromRunes(tx[cspi]) if lns.FontSize == 0 { lns.FontSize = sty.Size * fsz } nsp := sty.ToRunes() coff := rns.Start - cspSt cend := coff + rns.Len() crsz := len(cr) if coff >= crsz || cend > crsz { // fmt.Println("out of bounds:", string(cr), crsz, coff, cend) cend = min(crsz, cend) coff = min(crsz, coff) } if cend-coff == 0 { continue } nr := cr[coff:cend] // note: not a copy! nsp = append(nsp, nr...) lsp = append(lsp, nsp) // fmt.Println(sty, string(nr)) if cend > (cspEd - cspSt) { // shouldn't happen, to combine multiple original spans fmt.Println("combined original span:", cend, cspEd-cspSt, cspi, string(cr), "prev:", string(nr), "next:", string(cr[cend:])) } run.SetFromStyle(sty, tsty) if sty.IsMath() { mt := sh.maths[cspi] if mt != nil { run.Math = *mt run.MaxBounds = mt.BBox bb := run.MaxBounds.Translate(math32.Vector2FromFixed(pos)) ln.Bounds.ExpandByBox(bb) pos.X += math32.ToFixed(run.MaxBounds.Size().X) ysz := bb.Size().Y // fmt.Println("math ysz:", ysz, "maxAsc:", maxAsc) maxAsc = max(maxAsc, math32.ToFixed(-bb.Min.Y)) maxLHt = max(maxLHt, ysz) } } else { llht := tsty.LineHeightDots(sty) maxLHt = max(maxLHt, llht) bb := math32.B2FromFixed(run.RunBounds().Add(pos)) ln.Bounds.ExpandByBox(bb) // fmt.Println("adv:", pos, run.Output.Advance, bb.Size().X) pos = DirectionAdvance(run.Direction, pos, run.Output.Advance) } ln.Runs = append(ln.Runs, &run) } if li == 0 { // set offset for first line based on max ascent if !dir.IsVertical() { // todo: vertical! off.Y = math32.FromFixed(maxAsc) } } ln.Source = lsp // offset has prior line's size built into it, but we need to also accommodate // any extra size in _our_ line beyond what is expected. ourOff := off // fmt.Println(ln.Bounds) // advance offset: if dir.IsVertical() { lwd := ln.Bounds.Size().X extra := max(lwd-lns.LineHeight, 0) if dir.Progression() == di.FromTopLeft { // fmt.Println("ftl lwd:", lwd, off.X) off.X += lwd // ? ourOff.X += extra } else { // fmt.Println("!ftl lwd:", lwd, off.X) off.X -= lwd // ? ourOff.X -= extra } } else { // always top-down, no progression issues lby := ln.Bounds.Size().Y // the result at this point is centered with this height // which includes the natural line height property of the font itself. lpd := 0.5 * (maxLHt - lby) // half of diff if li > 0 { ourOff.Y += (lpd + (maxLHt - lns.LineHeight)) } ln.Bounds.Min.Y -= lpd ln.Bounds.Max.Y += lpd off.Y += maxLHt // fmt.Println("lby:", lby, fsz, maxLHt, lpd, ourOff.Y) } // go back through and give every run the expanded line-level box for ri := range ln.Runs { run := ln.Runs[ri] rb := run.LineBounds() if dir.IsVertical() { rb.Min.X, rb.Max.X = ln.Bounds.Min.X, ln.Bounds.Max.X rb.Min.Y -= 1 // ensure some overlap along direction of rendering adjacent rb.Max.Y += 1 } else { rb.Min.Y, rb.Max.Y = ln.Bounds.Min.Y, ln.Bounds.Max.Y rb.Min.X -= 1 rb.Max.Y += 1 } run.AsBase().MaxBounds = rb } ln.Offset = ourOff if tsty.WhiteSpace.HasWordWrap() && size.X > 0 && ln.Bounds.Size().X > size.X { // fmt.Println("size exceeded:", ln.Bounds.Size().X, size.X) ln.Bounds.Max.X -= ln.Bounds.Size().X - size.X } lns.Bounds.ExpandByBox(ln.Bounds.Translate(ln.Offset)) lns.Lines = append(lns.Lines, ln) } if lns.Bounds.Size().Y < lht { lns.Bounds.Max.Y = lns.Bounds.Min.Y + lht } // fmt.Println(lns.Bounds) lns.AlignX(tsty) return lns } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build !js package shapers import ( "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/shaped/shapers/shapedgt" ) func init() { shaped.NewShaper = shapedgt.NewShaper } // 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/text/parse/lexer" "cogentcore.org/core/text/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.Start += widx t.End += 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/text/spell" ) //go:generate core generate -add-types -add-funcs // Config is the configuration information for the dict 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/text/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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // note: adapted from https://github.com/tdewolff/canvas, // Copyright (c) 2015 Taco de Wolff, under an MIT License. package tex import ( "encoding/binary" "fmt" "cogentcore.org/core/paint/ppath" ) var debug = false type state struct { h, v, w, x, y, z int32 } // DVIToPath parses a DVI file (output from TeX) and returns *ppath.Path. // fontSizeDots specifies the actual font size in dots (actual pixels) // for a 10pt font in the DVI system. func DVIToPath(b []byte, fonts *dviFonts, fontSizeDots float32) (*ppath.Path, error) { // state var fnt uint32 // font index s := state{} stack := []state{} f := float32(1.0) // scale factor in mm/units mag := uint32(1000) // is set explicitly in preamble fnts := map[uint32]*dviFont{} // selected fonts for indices fontScale := fontSizeDots / 8 // factor for scaling font itself fontScaleFactor := fontSizeDots / 2.8 // factor for scaling the math // first position of baseline which will be the path's origin firstChar := true h0 := int32(0) v0 := int32(0) p := &ppath.Path{} r := &dviReader{b, 0} for 0 < r.len() { cmd := r.readByte() if cmd <= 127 { // set_char if firstChar { h0, v0 = s.h, s.v firstChar = false } c := uint32(cmd) if _, ok := fnts[fnt]; !ok { return nil, fmt.Errorf("bad command: font %v undefined at position %v", fnt, r.i) } if debug { fmt.Printf("\nchar font #%d, cid: %d, rune: %s, pos: (%v,%v)\n", fnt, c, string(rune(c)), f*float32(s.h), f*float32(s.v)) } w := int32(fnts[fnt].Draw(p, f*float32(s.h), f*float32(s.v), c, fontScale) / f) s.h += w } else if 128 <= cmd && cmd <= 131 { // set if firstChar { h0, v0 = s.h, s.v firstChar = false } n := int(cmd - 127) if r.len() < n { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } c := r.readUint32N(n) if _, ok := fnts[fnt]; !ok { return nil, fmt.Errorf("bad command: font %v undefined at position %v", fnt, r.i) } // fmt.Println("print:", string(rune(c)), s.v) s.h += int32(fnts[fnt].Draw(p, f*float32(s.h), f*float32(s.v), c, fontScale) / f) } else if cmd == 132 { // set_rule height := r.readInt32() width := r.readInt32() if 0 < width && 0 < height { p.MoveTo(f*float32(s.h), f*float32(s.v)) p.LineTo(f*float32(s.h+width), f*float32(s.v)) p.LineTo(f*float32(s.h+width), f*float32(s.v-height)) p.LineTo(f*float32(s.h), f*float32(s.v-height)) p.Close() } s.h += width } else if 133 <= cmd && cmd <= 136 { // put if firstChar { h0, v0 = s.h, s.v firstChar = false } n := int(cmd - 132) if r.len() < n { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } c := r.readUint32N(n) if _, ok := fnts[fnt]; !ok { return nil, fmt.Errorf("bad command: font %v undefined at position %v", fnt, r.i) } // fmt.Println("print:", string(rune(c)), s.v) fnts[fnt].Draw(p, f*float32(s.h), f*float32(s.v), c, fontScale) } else if cmd == 137 { // put_rule height := r.readInt32() width := r.readInt32() if 0 < width && 0 < height { p.MoveTo(f*float32(s.h), f*float32(s.v)) p.LineTo(f*float32(s.h+width), f*float32(s.v)) p.LineTo(f*float32(s.h+width), f*float32(s.v-height)) p.LineTo(f*float32(s.h), f*float32(s.v-height)) p.Close() } } else if cmd == 138 { // nop } else if cmd == 139 { // bop fnt = 0 s = state{0, 0, 0, 0, 0, 0} stack = stack[:0] _ = r.readBytes(10 * 4) _ = r.readUint32() // pointer } else if cmd == 140 { // eop } else if cmd == 141 { // push stack = append(stack, s) } else if cmd == 142 { // pop if len(stack) == 0 { return nil, fmt.Errorf("bad command: stack is empty at position %v", r.i) } s = stack[len(stack)-1] stack = stack[:len(stack)-1] } else if 143 <= cmd && cmd <= 146 { // right n := int(cmd - 142) if r.len() < n { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } d := r.readInt32N(n) s.h += d } else if 147 <= cmd && cmd <= 151 { // w if cmd == 147 { s.h += s.w } else { n := int(cmd - 147) if r.len() < n { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } d := r.readInt32N(n) s.w = d s.h += d } } else if 152 <= cmd && cmd <= 156 { // x if cmd == 152 { s.h += s.x } else { n := int(cmd - 152) if r.len() < n { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } d := r.readInt32N(n) s.x = d s.h += d } } else if 157 <= cmd && cmd <= 160 { // down n := int(cmd - 156) if r.len() < n { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } d := r.readInt32N(n) // fmt.Println("down:", d, s.v) s.v += d } else if 161 <= cmd && cmd <= 165 { // y if cmd == 161 { s.v += s.y } else { n := int(cmd - 152) if r.len() < n { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } d := r.readInt32N(n) s.y = d s.v += d } } else if 166 <= cmd && cmd <= 170 { // z if cmd == 166 { s.v += s.z fmt.Println("z down", s.z, s.v) } else { n := int(cmd - 166) if r.len() < n { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } d := r.readInt32N(n) s.z = d s.v += d } } else if 171 <= cmd && cmd <= 234 { // fnt_num fnt = uint32(cmd - 171) } else if 235 <= cmd && cmd <= 242 { // fnt n := int(cmd - 234) if r.len() < n { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } fnt = r.readUint32N(n) } else if 239 <= cmd && cmd <= 242 { // xxx n := int(cmd - 242) if r.len() < n { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } k := int(r.readUint32N(n)) if r.len() < k { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } _ = r.readBytes(k) } else if 243 <= cmd && cmd <= 246 { // fnt_def n := int(cmd - 242) if r.len() < n+14 { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } k := r.readUint32N(n) _ = r.readBytes(4) // checksum size := r.readUint32() design := r.readUint32() // design a := r.readByte() l := r.readByte() if r.len() < int(a+l) { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } _ = r.readString(int(a)) // area fscale := float32(mag) * float32(size) / 1000.0 / float32(design) // this is 1 for 10pt font: name := r.readString(int(l)) fnts[k] = fonts.Get(name, fscale) if debug { fmt.Printf("\ndefine font #:%d name: %s, size: %v, mag: %v, design: %v, scale: %v\n", k, name, size, mag, design, fscale) } } else if cmd == 247 { // pre _ = r.readByte() // version num := r.readUint32() den := r.readUint32() mag = r.readUint32() f = fontScaleFactor * float32(num) / float32(den) * float32(mag) / 1000.0 / 10000.0 // in units/mm // fmt.Println("num:", num, "mag:", mag, "den:", den, "f:", f) n := int(r.readByte()) _ = r.readString(n) // comment } else if cmd == 248 { _ = r.readUint32() // pointer to final bop _ = r.readUint32() // num _ = r.readUint32() // den _ = r.readUint32() // mag _ = r.readUint32() // largest height _ = r.readUint32() // largest width _ = r.readUint16() // maximum stack depth _ = r.readUint16() // number of pages } else if cmd == 249 { _ = r.readUint32() // pointer to post _ = r.readByte() // version for 0 < r.len() { if r.readByte() != 223 { break } } } else { return nil, fmt.Errorf("bad command: %v at position %v", cmd, r.i) } } // fmt.Println("start offsets:", h0, v0) *p = p.Translate(-f*float32(h0), -f*float32(v0)) return p, nil } type dviReader struct { b []byte i int } func (r *dviReader) len() int { return len(r.b) - r.i } func (r *dviReader) readByte() byte { r.i++ return r.b[r.i-1] } func (r *dviReader) readUint16() uint16 { num := binary.BigEndian.Uint16(r.b[r.i : r.i+2]) r.i += 2 return num } func (r *dviReader) readUint32() uint32 { num := binary.BigEndian.Uint32(r.b[r.i : r.i+4]) r.i += 4 return num } func (r *dviReader) readInt32() int32 { return int32(r.readUint32()) } func (r *dviReader) readUint32N(n int) uint32 { if n == 1 { return uint32(r.readByte()) } else if n == 2 { return uint32(r.readUint16()) } else if n == 3 { a := r.readByte() b := r.readByte() c := r.readByte() return uint32(a)<<16 | uint32(b)<<8 | uint32(c) } else if n == 4 { return r.readUint32() } r.i += n return 0 } func (r *dviReader) readInt32N(n int) int32 { if n == 3 { a := r.readByte() b := r.readByte() c := r.readByte() if a < 128 { return int32(uint32(a)<<16 | uint32(b)<<8 | uint32(c)) } return int32((uint32(a)-256)<<16 | uint32(b)<<8 | uint32(c)) } return int32(r.readUint32N(n)) } func (r *dviReader) readBytes(n int) []byte { b := r.b[r.i : r.i+n] r.i += n return b } func (r *dviReader) readString(n int) string { return string(r.readBytes(n)) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // note: adapted from https://github.com/tdewolff/canvas, // Copyright (c) 2015 Taco de Wolff, under an MIT License. // and gioui: Unlicense OR MIT, Copyright (c) 2019 The Gio authors package tex import ( "bytes" "fmt" "strconv" "cogentcore.org/core/base/errors" "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" "cogentcore.org/core/text/fonts" "github.com/go-fonts/latin-modern/lmmath" "github.com/go-fonts/latin-modern/lmmono10italic" "github.com/go-fonts/latin-modern/lmmono10regular" "github.com/go-fonts/latin-modern/lmmono12regular" "github.com/go-fonts/latin-modern/lmmono8regular" "github.com/go-fonts/latin-modern/lmmono9regular" "github.com/go-fonts/latin-modern/lmmonocaps10regular" "github.com/go-fonts/latin-modern/lmmonoslant10regular" "github.com/go-fonts/latin-modern/lmroman10bold" "github.com/go-fonts/latin-modern/lmroman10bolditalic" "github.com/go-fonts/latin-modern/lmroman10italic" "github.com/go-fonts/latin-modern/lmroman10regular" "github.com/go-fonts/latin-modern/lmroman12bold" "github.com/go-fonts/latin-modern/lmroman12italic" "github.com/go-fonts/latin-modern/lmroman12regular" "github.com/go-fonts/latin-modern/lmroman17regular" "github.com/go-fonts/latin-modern/lmroman5bold" "github.com/go-fonts/latin-modern/lmroman5regular" "github.com/go-fonts/latin-modern/lmroman6bold" "github.com/go-fonts/latin-modern/lmroman6regular" "github.com/go-fonts/latin-modern/lmroman7bold" "github.com/go-fonts/latin-modern/lmroman7italic" "github.com/go-fonts/latin-modern/lmroman7regular" "github.com/go-fonts/latin-modern/lmroman8bold" "github.com/go-fonts/latin-modern/lmroman8italic" "github.com/go-fonts/latin-modern/lmroman8regular" "github.com/go-fonts/latin-modern/lmroman9bold" "github.com/go-fonts/latin-modern/lmroman9italic" "github.com/go-fonts/latin-modern/lmroman9regular" "github.com/go-fonts/latin-modern/lmromancaps10regular" "github.com/go-fonts/latin-modern/lmromandunh10regular" "github.com/go-fonts/latin-modern/lmromanslant10bold" "github.com/go-fonts/latin-modern/lmromanslant10regular" "github.com/go-fonts/latin-modern/lmromanslant12regular" "github.com/go-fonts/latin-modern/lmromanslant17regular" "github.com/go-fonts/latin-modern/lmromanslant8regular" "github.com/go-fonts/latin-modern/lmromanslant9regular" "github.com/go-fonts/latin-modern/lmromanunsl10regular" "github.com/go-fonts/latin-modern/lmsans10bold" "github.com/go-fonts/latin-modern/lmsans10oblique" "github.com/go-fonts/latin-modern/lmsans10regular" "github.com/go-fonts/latin-modern/lmsans12oblique" "github.com/go-fonts/latin-modern/lmsans12regular" "github.com/go-fonts/latin-modern/lmsans17oblique" "github.com/go-fonts/latin-modern/lmsans17regular" "github.com/go-fonts/latin-modern/lmsans8oblique" "github.com/go-fonts/latin-modern/lmsans8regular" "github.com/go-fonts/latin-modern/lmsans9oblique" "github.com/go-fonts/latin-modern/lmsans9regular" "github.com/go-fonts/latin-modern/lmsansdemicond10regular" "github.com/go-fonts/latin-modern/lmsansquot8oblique" "github.com/go-fonts/latin-modern/lmsansquot8regular" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/font/opentype" ) const mmPerPt = 25.4 / 72.0 // LMFontsLoad loads the LMFonts. func LMFontsLoad() { for i := range LMFonts { fd := &LMFonts[i] errors.Log(fd.Load()) } } // LMFonts are tex latin-modern fonts. var LMFonts = []fonts.Data{ {Family: "cmbsy", Data: lmmath.TTF}, {Family: "cmr17", Data: lmroman17regular.TTF}, {Family: "cmr12", Data: lmroman12regular.TTF}, {Family: "cmr10", Data: lmroman10regular.TTF}, {Family: "cmr9", Data: lmroman9regular.TTF}, {Family: "cmr8", Data: lmroman8regular.TTF}, {Family: "cmr7", Data: lmroman7regular.TTF}, {Family: "cmr6", Data: lmroman6regular.TTF}, {Family: "cmr5", Data: lmroman5regular.TTF}, // cmb, cmbx {Family: "cmb12", Data: lmroman12bold.TTF}, {Family: "cmb10", Data: lmroman10bold.TTF}, {Family: "cmb9", Data: lmroman9bold.TTF}, {Family: "cmb8", Data: lmroman8bold.TTF}, {Family: "cmb7", Data: lmroman7bold.TTF}, {Family: "cmb6", Data: lmroman6bold.TTF}, {Family: "cmb5", Data: lmroman5bold.TTF}, // cmti {Family: "cmti12", Data: lmroman12italic.TTF}, {Family: "cmti10", Data: lmroman10italic.TTF}, {Family: "cmti9", Data: lmroman9italic.TTF}, {Family: "cmti8", Data: lmroman8italic.TTF}, {Family: "cmti7", Data: lmroman7italic.TTF}, // cmsl {Family: "cmsl17", Data: lmromanslant17regular.TTF}, {Family: "cmsl12", Data: lmromanslant12regular.TTF}, {Family: "cmsl10", Data: lmromanslant10regular.TTF}, {Family: "cmsl9", Data: lmromanslant9regular.TTF}, {Family: "cmsl8", Data: lmromanslant8regular.TTF}, // cmbxsl {Family: "cmbxsl10", Data: lmromanslant10bold.TTF}, // cmbxti, cmmib with cmapCMMI {Family: "cmmib10", Data: lmroman10bolditalic.TTF}, // cmcsc {Family: "cmcsc10", Data: lmromancaps10regular.TTF}, // cmdunh {Family: "cmdunh10", Data: lmromandunh10regular.TTF}, // cmu {Family: "cmu10", Data: lmromanunsl10regular.TTF}, // cmss {Family: "cmss17", Data: lmsans17regular.TTF}, {Family: "cmss12", Data: lmsans12regular.TTF}, {Family: "cmss10", Data: lmsans10regular.TTF}, {Family: "cmss9", Data: lmsans9regular.TTF}, {Family: "cmss8", Data: lmsans8regular.TTF}, // cmssb, cmssbx {Family: "cmssb10", Data: lmsans10bold.TTF}, // cmssdc {Family: "cmssdc10", Data: lmsansdemicond10regular.TTF}, // cmssi {Family: "cmssi17", Data: lmsans17oblique.TTF}, {Family: "cmssi12", Data: lmsans12oblique.TTF}, {Family: "cmssi10", Data: lmsans10oblique.TTF}, {Family: "cmssi9", Data: lmsans9oblique.TTF}, {Family: "cmssi8", Data: lmsans8oblique.TTF}, // cmssq {Family: "cmssq8", Data: lmsansquot8regular.TTF}, // cmssqi {Family: "cmssqi8", Data: lmsansquot8oblique.TTF}, // cmtt {Family: "cmtt12", Data: lmmono12regular.TTF}, {Family: "cmtt10", Data: lmmono10regular.TTF}, {Family: "cmtt9", Data: lmmono9regular.TTF}, {Family: "cmtt8", Data: lmmono8regular.TTF}, // cmti // {Family: "cmti", Data: lmmono12italic.TTF}, {Family: "cmti10", Data: lmmono10italic.TTF}, // {Family: "cmti", Data: lmmono9italic.TTF}, // {Family: "cmti", Data: lmmono8italic.TTF}, // cmtcsc {Family: "cmtcsc10", Data: lmmonocaps10regular.TTF}, } //////// dviFonts // dviFonts supports rendering of following standard DVI fonts: // // cmr: Roman (5--10pt) // cmmi: Math Italic (5--10pt) // cmsy: Math Symbols (5--10pt) // cmex: Math Extension (10pt) // cmss: Sans serif (10pt) // cmssqi: Sans serif quote italic (8pt) // cmssi: Sans serif Italic (10pt) // cmbx: Bold Extended (10pt) // cmtt: Typewriter (8--10pt) // cmsltt: Slanted typewriter (10pt) // cmsl: Slanted roman (8--10pt) // cmti: Text italic (7--10pt) // cmu: Unslanted text italic (10pt) // cmmib: Bold math italic (10pt) // cmbsy: Bold math symbols (10pt) // cmcsc: Caps and Small caps (10pt) // cmssbx: Sans serif bold extended (10pt) // cmdunh: Dunhill style (10pt) type dviFonts struct { font map[string]*dviFont mathSyms *dviFont // always available as backup for any rune } type dviFont struct { face *font.Face cmap map[uint32]rune size float32 italic bool ex bool mathSyms *dviFont // always available as backup for any rune } func newFonts() *dviFonts { return &dviFonts{ font: map[string]*dviFont{}, } } func (fs *dviFonts) Get(name string, scale float32) *dviFont { i := 0 for i < len(name) && 'a' <= name[i] && name[i] <= 'z' { i++ } fontname := name[:i] fontsize := float32(10.0) if ifontsize, err := strconv.Atoi(name[i:]); err == nil { fontsize = float32(ifontsize) } // fmt.Println("font name:", fontname, fontsize, scale) if fs.mathSyms == nil { fs.mathSyms = fs.loadFont("cmsy", cmapCMSY, 10.0, scale, lmmath.TTF) } cmap := cmapCMR f, ok := fs.font[name] if !ok { var fontSizes map[float32][]byte switch fontname { case "cmb", "cmbx": fontSizes = map[float32][]byte{ 12.0: lmroman12bold.TTF, 10.0: lmroman10bold.TTF, 9.0: lmroman9bold.TTF, 8.0: lmroman8bold.TTF, 7.0: lmroman7bold.TTF, 6.0: lmroman6bold.TTF, 5.0: lmroman5bold.TTF, } case "cmbsy": cmap = cmapCMSY fontSizes = map[float32][]byte{ fontsize: lmmath.TTF, } case "cmbxsl": fontSizes = map[float32][]byte{ fontsize: lmromanslant10bold.TTF, } case "cmbxti": fontSizes = map[float32][]byte{ 10.0: lmroman10bolditalic.TTF, } case "cmcsc": cmap = cmapCMTT fontSizes = map[float32][]byte{ 10.0: lmromancaps10regular.TTF, } case "cmdunh": fontSizes = map[float32][]byte{ 10.0: lmromandunh10regular.TTF, } case "cmex": cmap = cmapCMEX fontSizes = map[float32][]byte{ fontsize: lmmath.TTF, } case "cmitt": cmap = cmapCMTT fontSizes = map[float32][]byte{ 10.0: lmmono10italic.TTF, } case "cmmi": cmap = cmapCMMI fontSizes = map[float32][]byte{ 12.0: lmroman12italic.TTF, 10.0: lmroman10italic.TTF, 9.0: lmroman9italic.TTF, 8.0: lmroman8italic.TTF, 7.0: lmroman7italic.TTF, } case "cmmib": cmap = cmapCMMI fontSizes = map[float32][]byte{ 10.0: lmroman10bolditalic.TTF, } case "cmr": fontSizes = map[float32][]byte{ 17.0: lmroman17regular.TTF, 12.0: lmroman12regular.TTF, 10.0: lmroman10regular.TTF, 9.0: lmroman9regular.TTF, 8.0: lmroman8regular.TTF, 7.0: lmroman7regular.TTF, 6.0: lmroman6regular.TTF, 5.0: lmroman5regular.TTF, } case "cmsl": fontSizes = map[float32][]byte{ 17.0: lmromanslant17regular.TTF, 12.0: lmromanslant12regular.TTF, 10.0: lmromanslant10regular.TTF, 9.0: lmromanslant9regular.TTF, 8.0: lmromanslant8regular.TTF, } case "cmsltt": fontSizes = map[float32][]byte{ 10.0: lmmonoslant10regular.TTF, } case "cmss": fontSizes = map[float32][]byte{ 17.0: lmsans17regular.TTF, 12.0: lmsans12regular.TTF, 10.0: lmsans10regular.TTF, 9.0: lmsans9regular.TTF, 8.0: lmsans8regular.TTF, } case "cmssb", "cmssbx": fontSizes = map[float32][]byte{ 10.0: lmsans10bold.TTF, } case "cmssdc": fontSizes = map[float32][]byte{ 10.0: lmsansdemicond10regular.TTF, } case "cmssi": fontSizes = map[float32][]byte{ 17.0: lmsans17oblique.TTF, 12.0: lmsans12oblique.TTF, 10.0: lmsans10oblique.TTF, 9.0: lmsans9oblique.TTF, 8.0: lmsans8oblique.TTF, } case "cmssq": fontSizes = map[float32][]byte{ 8.0: lmsansquot8regular.TTF, } case "cmssqi": fontSizes = map[float32][]byte{ 8.0: lmsansquot8oblique.TTF, } case "cmsy": cmap = cmapCMSY fontSizes = map[float32][]byte{ fontsize: lmmath.TTF, } case "cmtcsc": cmap = cmapCMTT fontSizes = map[float32][]byte{ 10.0: lmmonocaps10regular.TTF, } //case "cmtex": //cmap = nil case "cmti": fontSizes = map[float32][]byte{ 12.0: lmroman12italic.TTF, 10.0: lmroman10italic.TTF, 9.0: lmroman9italic.TTF, 8.0: lmroman8italic.TTF, 7.0: lmroman7italic.TTF, } case "cmtt": cmap = cmapCMTT fontSizes = map[float32][]byte{ 12.0: lmmono12regular.TTF, 10.0: lmmono10regular.TTF, 9.0: lmmono9regular.TTF, 8.0: lmmono8regular.TTF, } case "cmu": fontSizes = map[float32][]byte{ 10.0: lmromanunsl10regular.TTF, } //case "cmvtt": //cmap = cmapCTT default: fmt.Println("WARNING: unknown font", fontname) } // select closest matching font size var data []byte var size float32 for isize, idata := range fontSizes { if data == nil || math32.Abs(isize-fontsize) < math32.Abs(size-fontsize) { data = idata size = isize } } f = fs.loadFont(fontname, cmap, fontsize, scale, data) fs.font[name] = f } return f } func (fs *dviFonts) loadFont(fontname string, cmap map[uint32]rune, fontsize, scale float32, data []byte) *dviFont { faces, err := font.ParseTTC(bytes.NewReader(data)) if err != nil { // todo: should still work presumably? errors.Log(err) } face := faces[0] fsize := scale * fontsize isItalic := 0 < len(fontname) && fontname[len(fontname)-1] == 'i' isEx := fontname == "cmex" return &dviFont{face: face, cmap: cmap, size: fsize, italic: isItalic, ex: isEx, mathSyms: fs.mathSyms} } const ( mag1 = 1.2 mag2 = 1.2 * 1.2 mag3 = 1.2 * 1.2 * 1.2 mag4 = 1.2 * 1.2 * 1.2 * 1.2 * 1.2 mag5 = 3.2 ) var cmexScales = map[uint32]float32{ 0x00: mag1, 0x01: mag1, 0x02: mag1, 0x03: mag1, 0x04: mag1, 0x05: mag1, 0x06: mag1, 0x07: mag1, 0x08: mag1, 0x0A: mag1, 0x0B: mag1, 0x0C: mag1, 0x0D: mag1, 0x0E: mag1, 0x0F: mag1, 0x10: mag3, // ( 0x11: mag3, // ) 0x12: mag4, // ( 0x13: mag4, // ) 0x14: mag4, // [ 0x15: mag4, // ] 0x16: mag4, // ⌊ 0x17: mag4, // ⌋ 0x18: mag4, // ⌈ 0x19: mag4, // ⌉ 0x1A: mag4, // { 0x1B: mag4, // } 0x1C: mag4, // 〈 0x1D: mag4, // 〉 0x1E: mag4, // ∕ 0x1F: mag4, // \ 0x20: mag5, // ( 0x21: mag5, // ) 0x22: mag5, // [ 0x23: mag5, // ] 0x24: mag5, // ⌊ 0x25: mag5, // ⌋ 0x26: mag5, // ⌈ 0x27: mag5, // ⌉ 0x28: mag5, // { 0x29: mag5, // } 0x2A: mag5, // 〈 0x2B: mag5, // 〉 0x2C: mag5, // ∕ 0x2D: mag5, // \ 0x2E: mag3, // ∕ 0x2F: mag3, // \ 0x30: mag2, // ⎛ 0x31: mag2, // ⎞ 0x32: mag2, // ⌈ 0x33: mag2, // ⌉ 0x34: mag2, // ⌊ 0x35: mag2, // ⌋ 0x36: mag2, // ⎢ 0x37: mag2, // ⎥ 0x38: mag2, // ⎧ // big braces start 0x39: mag2, // ⎫ 0x3A: mag2, // ⎩ 0x3B: mag2, // ⎭ 0x3C: mag2, // ⎨ 0x3D: mag2, // ⎬ 0x3E: mag2, // ⎪ 0x3F: mag2, // ∣ ?? unclear 0x40: mag2, // ⎝ // big parens 0x41: mag2, // ⎠ 0x42: mag2, // ⎜ 0x43: mag2, // ⎟ 0x44: mag2, // 〈 0x45: mag2, // 〉 0x47: mag2, // ⨆ 0x49: mag2, // ∮ 0x4B: mag2, // ⨀ 0x4D: mag2, // ⨁ 0x4F: mag2, // ⨂ 0x58: mag2, // ∑ 0x59: mag2, // ∏ 0x5A: mag2, // ∫ 0x5B: mag2, // ⋃ 0x5C: mag2, // ⋂ 0x5D: mag2, // ⨄ 0x5E: mag2, // ⋀ 0x5F: mag2, // ⋁ 0x61: mag2, // ∐ 0x63: mag2, // ̂ 0x64: mag4, // ̂ 0x66: mag2, // ˜ 0x67: mag4, // ˜ 0x68: mag3, // [ 0x69: mag3, // ] 0x6B: mag2, // ⌋ 0x6C: mag2, // ⌈ 0x6D: mag2, // ⌉ 0x6E: mag3, // { 0x6F: mag3, // } 0x71: mag3, // √ 0x72: mag4, // √ 0x73: mag5, // √ 0x74: mag1, // ⎷ 0x75: mag1, // ⏐ 0x76: mag1, // ⌜ } func (f *dviFont) Draw(p *ppath.Path, x, y float32, cid uint32, scale float32) float32 { r := f.cmap[cid] face := f.face gid, ok := face.Cmap.Lookup(r) if !ok { if f.mathSyms != nil { face = f.mathSyms.face gid, ok = face.Cmap.Lookup(r) if !ok { fmt.Println("rune not found in mathSyms:", string(r)) } } else { fmt.Println("rune not found:", string(r)) } } hadv := face.HorizontalAdvance(gid) // fmt.Printf("rune: 0x%0x gid: %d, r: 0x%0X\n", cid, gid, int(r)) outline := face.GlyphData(gid).(font.GlyphOutline) sc := scale * f.size / float32(face.Upem()) xsc := float32(1) // fmt.Println("draw scale:", sc, "f.size:", f.size, "face.Upem()", face.Upem()) if f.ex { ext, _ := face.GlyphExtents(gid) exsc, has := cmexScales[cid] yb := ext.YBearing if has { sc *= exsc switch cid { case 0x5A, 0x49: // \int and \oint are off in large size yb += 200 case 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F: // larger delims are too thick xsc = .7 case 0x20, 0x21, 0x22, 0x23, 0x28, 0x29, 0x2A, 0x2B: // same for even larger ones xsc = .6 case 0x3C, 0x3D: // braces middles need shifting yb += 150 case 0x3A, 0x3B: // braces bottom shifting yb += 400 // below are fixes for all the square root elements case 0x71: x += sc * 80 xsc = .6 case 0x72: x -= sc * 80 xsc = .6 case 0x73: x -= sc * 80 xsc = .5 case 0x74: yb += 600 case 0x75: x += sc * 560 case 0x76: x += sc * 400 yb -= 36 } } y += sc * yb } if f.italic { // angle := f.face.Post.ItalicAngle // angle := float32(-15) // degrees // x -= scale * f.size * face.LineMetric(font.XHeight) / 2.0 * math32.Tan(-angle*math.Pi/180.0) } for _, s := range outline.Segments { p0 := math32.Vec2(s.Args[0].X*xsc*sc+x, -s.Args[0].Y*sc+y) switch s.Op { case opentype.SegmentOpMoveTo: p.MoveTo(p0.X, p0.Y) case opentype.SegmentOpLineTo: p.LineTo(p0.X, p0.Y) case opentype.SegmentOpQuadTo: p1 := math32.Vec2(s.Args[1].X*xsc*sc+x, -s.Args[1].Y*sc+y) p.QuadTo(p0.X, p0.Y, p1.X, p1.Y) case opentype.SegmentOpCubeTo: p1 := math32.Vec2(s.Args[1].X*xsc*sc+x, -s.Args[1].Y*sc+y) p2 := math32.Vec2(s.Args[2].X*xsc*sc+x, -s.Args[2].Y*sc+y) p.CubeTo(p0.X, p0.Y, p1.X, p1.Y, p2.X, p2.Y) } } p.Close() adv := sc * hadv // fmt.Println("hadv:", face.HorizontalAdvance(gid), "adv:", adv) return adv } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package tex import ( "bytes" "fmt" "strings" "sync" "cogentcore.org/core/paint/ppath" "cogentcore.org/core/text/shaped" "star-tex.org/x/tex" ) var ( texEngine *tex.Engine texFonts *dviFonts texMu sync.Mutex preamble = `\nopagenumbers \def\frac#1#2{{{#1}\over{#2}}} ` ) func init() { shaped.ShapeMath = TeXMath } // TeXMath parses a plain TeX math expression and returns a path // rendering that expression. This is NOT LaTeX and only \frac is defined // as an additional math utility function, for fractions. // To activate display math mode, add an additional $ $ surrounding the // expression: one set of $ $ is automatically included to produce inline // math mode rendering. // fontSizeDots specifies the actual font size in dots (actual pixels) // for a 10pt font in the DVI system. func TeXMath(formula string, fontSizeDots float32) (*ppath.Path, error) { texMu.Lock() defer texMu.Unlock() r := strings.NewReader(fmt.Sprintf(`%s $%s$ \bye `, preamble, formula)) w := &bytes.Buffer{} stdout := &bytes.Buffer{} if texEngine == nil { texEngine = tex.New() } texEngine.Stdout = stdout if err := texEngine.Process(w, r); err != nil { fmt.Println(stdout.String()) return nil, err } if texFonts == nil { texFonts = newFonts() } p, err := DVIToPath(w.Bytes(), texFonts, fontSizeDots) if err != nil { fmt.Println(stdout.String()) return nil, err } return p, nil } // Code generated by "core generate"; DO NOT EDIT. package text import ( "cogentcore.org/core/enums" ) var _AlignsValues = []Aligns{0, 1, 2, 3} // AlignsN is the highest valid value for type Aligns, plus one. const AlignsN Aligns = 4 var _AlignsValueMap = map[string]Aligns{`start`: 0, `end`: 1, `center`: 2, `justify`: 3} var _AlignsDescMap = map[Aligns]string{0: `Start aligns to the start (top, left) of text region.`, 1: `End aligns to the end (bottom, right) of text region.`, 2: `Center aligns to the center of text region.`, 3: `Justify spreads words to cover the entire text region.`} var _AlignsMap = map[Aligns]string{0: `start`, 1: `end`, 2: `center`, 3: `justify`} // 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 _WhiteSpacesValues = []WhiteSpaces{0, 1, 2, 3, 4, 5} // WhiteSpacesN is the highest valid value for type WhiteSpaces, plus one. const WhiteSpacesN WhiteSpaces = 6 var _WhiteSpacesValueMap = map[string]WhiteSpaces{`WrapAsNeeded`: 0, `WrapAlways`: 1, `WrapSpaceOnly`: 2, `WrapNever`: 3, `Pre`: 4, `PreWrap`: 5} var _WhiteSpacesDescMap = map[WhiteSpaces]string{0: `WrapAsNeeded means that all white space is collapsed to a single space, and text wraps at white space except if there is a long word that cannot fit on the next line, or would otherwise be truncated. 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: `WrapAlways is like [WrapAsNeeded] except that line wrap will always occur within words if it allows more content to fit on a line.`, 2: `WrapSpaceOnly means that line wrapping only occurs at white space, and never within words. This means that long words may then exceed the available space and will be truncated. White space is collapsed to a single space.`, 3: `WrapNever means that lines are never wrapped to fit. If there is an explicit line or paragraph break, that will still result in a new line. In general you also don't want simple non-wrapping text labels to Grow (GrowWrap = false). Use the SetTextWrap method to set both. White space is collapsed to a single space.`, 4: `WhiteSpacePre means that whitespace is preserved, including line breaks. Text will only wrap on explicit line or paragraph breaks. This acts like the <pre> tag in HTML.`, 5: `WhiteSpacePreWrap means that whitespace is preserved. Text will wrap when necessary, and on line breaks`} var _WhiteSpacesMap = map[WhiteSpaces]string{0: `WrapAsNeeded`, 1: `WrapAlways`, 2: `WrapSpaceOnly`, 3: `WrapNever`, 4: `Pre`, 5: `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) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package text import ( "cogentcore.org/core/text/rich" ) // Font is a compact encoding of font properties, which can be used // to reconstruct the corresponding [rich.Style] from [text.Style]. type Font struct { // StyleRune is the rune-compressed version of the [rich.Style] parameters. StyleRune rune // Size is the Text.Style.FontSize.Dots value of the font size, // multiplied by font rich.Style.Size. Size float32 // Family is a nonstandard family name: if standard, then empty, // and value is determined by [rich.DefaultSettings] and Style.Family. Family string } func NewFont(fsty *rich.Style, tsty *Style) *Font { fn := &Font{StyleRune: rich.RuneFromStyle(fsty), Size: tsty.FontHeight(fsty)} if fsty.Family == rich.Custom { fn.Family = string(tsty.CustomFont) } return fn } // Style returns the [rich.Style] version of this Font. func (fn *Font) Style(tsty *Style) *rich.Style { sty := rich.NewStyle() rich.RuneToStyle(sty, fn.StyleRune) sty.Size = fn.Size / tsty.FontSize.Dots return sty } // FontFamily returns the string value of the font Family for given [rich.Style], // using [text.Style] CustomFont or [rich.DefaultSettings] values. func (ts *Style) FontFamily(sty *rich.Style) string { if sty.Family == rich.Custom { return string(ts.CustomFont) } return sty.FontFamily(&rich.DefaultSettings) } func (fn *Font) FamilyString(tsty *Style) string { if fn.Family != "" { return fn.Family } return tsty.FontFamily(fn.Style(tsty)) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package text import ( "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/styles/styleprops" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/rich" ) // FromProperties sets style field values based on the given property list. func (s *Style) FromProperties(parent *Style, 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 } s.FromProperty(parent, key, val, ctxt) } } // FromProperty sets style field values based on the given property key and value. func (s *Style) FromProperty(parent *Style, key string, val any, cc colors.Context) { if sfunc, ok := styleFuncs[key]; ok { if parent != nil { sfunc(s, key, val, parent, cc) } else { sfunc(s, key, val, nil, cc) } return } } // styleFuncs are functions for styling the text.Style object. var styleFuncs = map[string]styleprops.Func{ "text-align": styleprops.Enum(Start, func(obj *Style) enums.EnumSetter { return &obj.Align }), "text-vertical-align": styleprops.Enum(Start, func(obj *Style) enums.EnumSetter { return &obj.AlignV }), // note: text-style reads the font-size setting for regular units cases. "font-size": styleprops.Units(units.Value{}, func(obj *Style) *units.Value { return &obj.FontSize }), "line-height": styleprops.FloatProportion(float32(1.2), func(obj *Style) *float32 { return &obj.LineHeight }), "line-spacing": styleprops.FloatProportion(float32(1.2), func(obj *Style) *float32 { return &obj.LineHeight }), "para-spacing": styleprops.FloatProportion(float32(1.2), func(obj *Style) *float32 { return &obj.ParaSpacing }), "white-space": styleprops.Enum(WrapAsNeeded, func(obj *Style) enums.EnumSetter { return &obj.WhiteSpace }), "direction": styleprops.Enum(rich.LTR, func(obj *Style) enums.EnumSetter { return &obj.Direction }), "text-indent": styleprops.Units(units.Value{}, func(obj *Style) *units.Value { return &obj.Indent }), "tab-size": styleprops.Int(int(4), func(obj *Style) *int { return &obj.TabSize }), "select-color": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Style) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.SelectColor = parent.(*Style).SelectColor } else if init { fs.SelectColor = colors.Scheme.Select.Container } return } fs.SelectColor = errors.Log1(gradient.FromAny(val, cc)) }, "highlight-color": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Style) if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.HighlightColor = parent.(*Style).HighlightColor } else if init { fs.HighlightColor = colors.Scheme.Warn.Container } return } fs.HighlightColor = errors.Log1(gradient.FromAny(val, cc)) }, } // ToProperties sets map[string]any properties based on non-default style values. // properties map must be non-nil. func (s *Style) ToProperties(sty *rich.Style, p map[string]any) { if s.FontSize.Unit != units.UnitDp || s.FontSize.Value != 16 || sty.Size != 1 { sz := s.FontSize sz.Value *= sty.Size p["font-size"] = sz.StringCSS() } if sty.Family == rich.Custom && s.CustomFont != "" { p["font-family"] = s.CustomFont } if sty.Slant != rich.SlantNormal { p["font-style"] = sty.Slant.String() } if sty.Weight != rich.Normal { p["font-weight"] = sty.Weight.String() } if sty.Stretch != rich.StretchNormal { p["font-stretch"] = sty.Stretch.String() } if sty.Decoration != 0 { p["text-decoration"] = sty.Decoration.String() } if s.Align != Start { p["text-align"] = s.Align.String() } if s.AlignV != Start { p["text-vertical-align"] = s.AlignV.String() } if s.LineHeight != 1.2 { p["line-height"] = reflectx.ToString(s.LineHeight) } if s.WhiteSpace != WrapAsNeeded { p["white-space"] = s.WhiteSpace.String() } if sty.Direction != rich.LTR { p["direction"] = s.Direction.String() } if s.TabSize != 4 { p["tab-size"] = reflectx.ToString(s.TabSize) } if sty.Decoration.HasFlag(rich.FillColor) { p["fill"] = colors.AsHex(sty.FillColor()) } else { p["fill"] = colors.AsHex(s.Color) } if s.SelectColor != nil { p["select-color"] = colors.AsHex(colors.ToUniform(s.SelectColor)) } if s.HighlightColor != nil { p["highlight-color"] = colors.AsHex(colors.ToUniform(s.HighlightColor)) } } // 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 // 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"` } func (es *EditorSettings) Defaults() { es.TabSize = 4 es.SpaceIndent = false } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package text import ( "image" "image/color" "cogentcore.org/core/colors" "cogentcore.org/core/math32" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/rich" ) //go:generate core generate // IMPORTANT: any changes here must be updated in props.go // note: the go-text shaping framework does not support letter spacing // or word spacing. These are uncommonly adjusted and not compatible with // internationalized text in any case. // todo: bidi override? // Style is used for text layout styling. // Most of these are inherited type Style struct { //types:add // Align specifies how to align text along the default direction (inherited). // This *only* applies to the text within its containing element, // and is relevant only for multi-line text. Align Aligns // AlignV specifies "vertical" (orthogonal to default direction) // 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 // FontSize is the default font size. The rich text styling specifies // sizes relative to this value, with the normal text size factor = 1. // In the [styles.Style.Text] context, this is copied from [styles.Font.Size]. FontSize units.Value // LineHeight is a multiplier on the default font size for spacing between lines. // If there are larger font elements within a line, they will be accommodated, with // the same amount of total spacing added above that maximum size as if it was all // the same height. The default of 1.3 represents standard "single spaced" text. LineHeight float32 `default:"1.3"` // ParaSpacing is the line spacing between paragraphs (inherited). // This will be copied from [Style.Margin] if that is non-zero, // or can be set directly. Like [LineHeight], this is a multiplier on // the default font size. ParaSpacing float32 `default:"1.5"` // 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 // Direction specifies the default text direction, which can be overridden if the // unicode text is typically written in a different direction. Direction rich.Directions // Indent specifies how much to indent the first line in a paragraph (inherited). Indent units.Value // TabSize specifies the tab size, in number of characters (inherited). TabSize int // Color is the default font fill color, used for inking fonts unless otherwise // specified in the [rich.Style]. Color color.Color // SelectColor is the color to use for the background region of selected text (inherited). SelectColor image.Image // HighlightColor is the color to use for the background region of highlighted text (inherited). HighlightColor image.Image // CustomFont specifies the Custom font name for rich.Style.Family = Custom. CustomFont rich.FontName } func NewStyle() *Style { s := &Style{} s.Defaults() return s } func (ts *Style) Defaults() { ts.Align = Start ts.AlignV = Start ts.FontSize.Dp(16) ts.LineHeight = 1.3 ts.ParaSpacing = 1.5 ts.Direction = rich.LTR ts.TabSize = 4 ts.Color = colors.ToUniform(colors.Scheme.OnSurface) ts.SelectColor = colors.Scheme.Select.Container ts.HighlightColor = colors.Scheme.Warn.Container } // ToDots runs ToDots on unit values, to compile down to raw pixels func (ts *Style) ToDots(uc *units.Context) { ts.FontSize.ToDots(uc) ts.FontSize.Dots = math32.Round(ts.FontSize.Dots) ts.Indent.ToDots(uc) } // InheritFields from parent func (ts *Style) InheritFields(parent *Style) { ts.Align = parent.Align ts.AlignV = parent.AlignV ts.LineHeight = parent.LineHeight ts.ParaSpacing = parent.ParaSpacing // ts.WhiteSpace = par.WhiteSpace // todo: we can't inherit this b/c label base default then gets overwritten ts.Direction = parent.Direction ts.Indent = parent.Indent ts.TabSize = parent.TabSize ts.SelectColor = parent.SelectColor ts.HighlightColor = parent.HighlightColor } // FontHeight returns the effective font height based on // FontSize * [rich.Style] Size multiplier. func (ts *Style) FontHeight(sty *rich.Style) float32 { return math32.Round(ts.FontSize.Dots * sty.Size) } // LineHeightDots returns the effective line height in dots (actual pixels) // as FontHeight * LineHeight func (ts *Style) LineHeightDots(sty *rich.Style) float32 { return math32.Ceil(ts.FontHeight(sty) * ts.LineHeight) } // AlignFactors gets basic text alignment factors func (ts *Style) AlignFactors() (ax, ay float32) { ax = ts.Align.Factor() 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 } // Aligns has the different types of alignment and justification for // the text. type Aligns int32 //enums:enum -transform kebab const ( // Start aligns to the start (top, left) of text region. Start Aligns = iota // End aligns to the end (bottom, right) of text region. End // Center aligns to the center of text region. Center // Justify spreads words to cover the entire text region. Justify ) // Factor returns the alignment factor (0, .5, 1). func (al Aligns) Factor() float32 { switch al { case Start: return 0 case Center: return 0.5 case End: return 1 } return 0 } // WhiteSpaces determine how white space is processed and line wrapping // occurs, either only at whitespace or within words. type WhiteSpaces int32 //enums:enum -trim-prefix WhiteSpace const ( // WrapAsNeeded means that all white space is collapsed to a single // space, and text wraps at white space except if there is a long word // that cannot fit on the next line, or would otherwise be truncated. // 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. WrapAsNeeded WhiteSpaces = iota // WrapAlways is like [WrapAsNeeded] except that line wrap will always // occur within words if it allows more content to fit on a line. WrapAlways // WrapSpaceOnly means that line wrapping only occurs at white space, // and never within words. This means that long words may then exceed // the available space and will be truncated. White space is collapsed // to a single space. WrapSpaceOnly // WrapNever means that lines are never wrapped to fit. If there is an // explicit line or paragraph break, that will still result in // a new line. In general you also don't want simple non-wrapping // text labels to Grow (GrowWrap = false). Use the SetTextWrap method // to set both. White space is collapsed to a single space. WrapNever // WhiteSpacePre means that whitespace is preserved, including line // breaks. Text will only wrap on explicit line or paragraph breaks. // This acts like the <pre> tag in HTML. WhiteSpacePre // WhiteSpacePreWrap means that whitespace is preserved. // Text will wrap when necessary, and on line breaks WhiteSpacePreWrap ) // HasWordWrap returns true if value supports word wrap. func (ws WhiteSpaces) HasWordWrap() bool { switch ws { case WrapNever, WhiteSpacePre: return false default: return true } } // KeepWhiteSpace returns true if value preserves existing whitespace. func (ws WhiteSpaces) KeepWhiteSpace() bool { switch ws { case WhiteSpacePre, WhiteSpacePreWrap: return true default: return false } } // SetUnitContext sets the font-specific information in the given // units.Context, based on the given styles. Just uses standardized // fractions of the font size for the other less common units such as ex, ch. func (ts *Style) SetUnitContext(uc *units.Context) { fsz := math32.Round(ts.FontSize.Dots) if fsz == 0 { fsz = 16 } uc.SetFont(fsz) } // TODO(text): ? // 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 // ) // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package textcore //go:generate core generate import ( "image" "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/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/styles/states" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/highlighting" "cogentcore.org/core/text/lines" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/text" "cogentcore.org/core/text/textpos" ) // 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"` ) // Base is a widget with basic infrastructure for viewing and editing // [lines.Lines] of monospaced text, used in [textcore.Editor] and // terminal. There can be multiple Base widgets for each lines buffer. // // Use NeedsRender to drive an render update for any change that does // not change the line-level layout of the text. // // All updating in the Base should be within a single goroutine, // as it would require extensive protections throughout code otherwise. type Base struct { //core:embedder core.Frame // Lines is the text lines content for this editor. Lines *lines.Lines `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 // AutoscrollOnInput scrolls the display to the end when Input events are received. AutoscrollOnInput bool // viewId is the unique id of the Lines view. viewId int // charSize is the render size of one character (rune). // Y = line height, X = total glyph advance. charSize math32.Vector2 // visSizeAlloc is the Geom.Size.Alloc.Total subtracting extra space, // available for rendering text lines and line numbers. visSizeAlloc math32.Vector2 // lastVisSizeAlloc is the last visSizeAlloc used in laying out lines. // It is used to trigger a new layout only when needed. lastVisSizeAlloc math32.Vector2 // visSize is the height in lines and width in chars of the visible area. visSize image.Point // linesSize is the height in lines and width in chars of the Lines text area, // (excluding line numbers), which can be larger than the visSize. linesSize image.Point // scrollPos is the position of the scrollbar, in units of lines of text. // fractional scrolling is supported. scrollPos float32 // hasLineNumbers indicates that this editor has line numbers // (per [Editor] option) hasLineNumbers bool // lineNumberOffset is the horizontal offset in chars for the start of text // after line numbers. This is 0 if no line numbers. lineNumberOffset int // totalSize is total size of all text, including line numbers, // multiplied by charSize. totalSize math32.Vector2 // lineNumberDigits is the number of line number digits needed. lineNumberDigits int // CursorPos is the current cursor position. CursorPos textpos.Pos `set:"-" edit:"-" json:"-" xml:"-"` // 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 // isScrolling is true when scrolling: prevents keeping current cursor position // in view. isScrolling bool // cursorTarget is the target cursor position for externally set targets. // It ensures that the target position is visible. cursorTarget textpos.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 textpos.Pos // SelectRegion is the current selection region. SelectRegion textpos.Region `set:"-" edit:"-" json:"-" xml:"-"` // previousSelectRegion is the previous selection region that was actually rendered. // It is needed to update the render. previousSelectRegion textpos.Region // Highlights is a slice of regions representing the highlighted // regions, e.g., for search results. Highlights []textpos.Region `set:"-" edit:"-" json:"-" xml:"-"` // scopelights is a slice of regions representing the highlighted // regions specific to scope markers. scopelights []textpos.Region // LinkHandler handles link clicks. // If it is nil, they are sent to the standard web URL handler. LinkHandler func(tl *rich.Hyperlink) // lineRenders are the cached rendered lines of text. lineRenders []renderCache // lineNoRenders are the cached rendered line numbers lineNoRenders []renderCache // tabRender is a shaped tab tabRender *shaped.Lines // selectMode is a boolean indicating whether to select text as the cursor moves. selectMode 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 string } func (ed *Base) WidgetValue() any { return ed.Lines.Text() } func (ed *Base) SetWidgetValue(value any) error { ed.Lines.SetString(reflectx.ToString(value)) return nil } func (ed *Base) Init() { ed.Frame.Init() ed.Styles.Font.Family = rich.Monospace // critical ed.SetLines(lines.NewLines()) ed.Styler(func(s *styles.Style) { s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Hoverable, abilities.Slideable, abilities.DoubleClickable, abilities.TripleClickable) s.SetAbilities(false, abilities.ScrollableUnattended) ed.CursorWidth.Dp(1) ed.LineNumberColor = nil 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.Base.WordWrap { // s.Text.WhiteSpace = styles.WhiteSpacePreWrap // } else { // s.Text.WhiteSpace = styles.WhiteSpacePre // } s.Text.LineHeight = 1.3 s.Text.WhiteSpace = text.WrapNever s.Font.Family = rich.Monospace 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 = text.Start s.Text.AlignV = text.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 if s.IsReadOnly() { s.Background = colors.Scheme.SurfaceContainer } // 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.OnClose(func(e events.Event) { ed.editDone() }) } func (ed *Base) Destroy() { ed.stopCursor() ed.Frame.Destroy() } func (ed *Base) NumLines() int { if ed.Lines != nil { return ed.Lines.NumLines() } return 0 } // 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 *Base) editDone() { if ed.Lines != nil { ed.Lines.EditDone() // sends the change event } ed.clearSelected() ed.clearCursor() } // 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 *Base) reMarkup() { if ed.Lines == nil { return } ed.Lines.ReMarkup() } // IsNotSaved returns true if buffer was changed (edited) since last Save. func (ed *Base) IsNotSaved() bool { return ed.Lines != nil && ed.Lines.IsNotSaved() } // Clear resets all the text in the buffer for this editor. func (ed *Base) Clear() { if ed.Lines == nil { return } ed.Lines.SetText([]byte{}) } // resetState resets all the random state variables, when opening a new buffer etc func (ed *Base) resetState() { ed.SelectReset() ed.Highlights = nil ed.scopelights = nil if ed.Lines == nil || ed.lastFilename != ed.Lines.Filename() { // don't reset if reopening.. ed.CursorPos = textpos.Pos{} } } // SendInput sends the [events.Input] event, for fine-grained updates. func (ed *Base) SendInput() { ed.Send(events.Input) } // SendClose sends the [events.Close] event, when lines buffer is closed. func (ed *Base) SendClose() { ed.Send(events.Close) } // SetLines sets the [lines.Lines] that this is an editor of, // creating a new view for this editor and connecting to events. func (ed *Base) SetLines(ln *lines.Lines) *Base { oldln := ed.Lines if ed == nil || (ln != nil && oldln == ln) { return ed } if oldln != nil { oldln.DeleteView(ed.viewId) ed.viewId = -1 } ed.Lines = ln ed.resetState() if ln != nil { ln.Settings.EditorSettings = core.SystemSettings.Editor wd := ed.linesSize.X if wd == 0 { wd = 80 } ed.viewId = ln.NewView(wd) ln.OnChange(ed.viewId, func(e events.Event) { ed.validateCursor() // could have changed with remarkup ed.NeedsRender() ed.SendChange() }) ln.OnInput(ed.viewId, func(e events.Event) { if ed.AutoscrollOnInput { ed.SetCursorTarget(textpos.PosErr) // special code to go to end } ed.NeedsRender() ed.SendInput() }) ln.OnClose(ed.viewId, func(e events.Event) { ed.SetLines(nil) ed.SendClose() }) phl := ln.PosHistoryLen() if phl > 0 { cp, _ := ln.PosHistoryAt(phl - 1) ed.posHistoryIndex = phl - 1 ed.SetCursorShow(cp) } else { ed.SetCursorShow(textpos.Pos{}) } } ed.NeedsRender() return ed } // styleBase applies the editor styles. func (ed *Base) styleBase() { if ed.NeedsRebuild() { highlighting.UpdateFromTheme() if ed.Lines != nil { ed.Lines.SetHighlighting(core.AppearanceSettings.Highlighting) } } ed.Frame.Style() ed.CursorWidth.ToDots(&ed.Styles.UnitContext) } func (ed *Base) Style() { ed.styleBase() ed.styleSizes() } // 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 textcore import ( "fmt" "strings" "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/text/lines" "cogentcore.org/core/text/parse" "cogentcore.org/core/text/parse/complete" "cogentcore.org/core/text/parse/parser" "cogentcore.org/core/text/textpos" ) // setCompleter sets completion functions so that completions will // automatically be offered as the user types func (ed *Editor) setCompleter(data any, matchFun complete.MatchFunc, editFun complete.EditFunc, lookupFun complete.LookupFunc) { if ed.Complete != nil { if ed.Complete.Context == data { ed.Complete.MatchFunc = matchFun ed.Complete.EditFunc = editFun ed.Complete.LookupFunc = lookupFun return } ed.deleteCompleter() } ed.Complete = core.NewComplete().SetContext(data).SetMatchFunc(matchFun). SetEditFunc(editFun).SetLookupFunc(lookupFun) ed.Complete.OnSelect(func(e events.Event) { ed.completeText(ed.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 (ed *Editor) deleteCompleter() { if ed.Complete == nil { return } ed.Complete.Cancel() ed.Complete = nil } // completeText edits the text using the string chosen from the completion menu func (ed *Editor) 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 := textpos.Pos{ed.Complete.SrcLn, 0} en := textpos.Pos{ed.Complete.SrcLn, ed.Lines.LineLen(ed.Complete.SrcLn)} var tbes string tbe := ed.Lines.Region(st, en) if tbe != nil { tbes = string(tbe.ToBytes()) } c := ed.Complete.GetCompletion(s) pos := textpos.Pos{ed.Complete.SrcLn, ed.Complete.SrcCh} ced := ed.Complete.EditFunc(ed.Complete.Context, tbes, ed.Complete.SrcCh, c, ed.Complete.Seed) if ced.ForwardDelete > 0 { delEn := textpos.Pos{ed.Complete.SrcLn, ed.Complete.SrcCh + ced.ForwardDelete} ed.Lines.DeleteText(pos, delEn) } // now the normal completion insertion st = pos st.Char -= len(ed.Complete.Seed) ed.Lines.ReplaceText(st, pos, st, ced.NewText, lines.ReplaceNoMatchCase) ep := st ep.Char += len(ced.NewText) + ced.CursorAdjust ed.SetCursorShow(ep) } // offerComplete pops up a menu of possible completions func (ed *Editor) offerComplete() { if ed.Complete == nil || ed.ISearch.On || ed.QReplace.On || ed.IsDisabled() { return } ed.Complete.Cancel() if !ed.Lines.Settings.Completion { return } if ed.Lines.InComment(ed.CursorPos) || ed.Lines.InLitString(ed.CursorPos) { return } ed.Complete.SrcLn = ed.CursorPos.Line ed.Complete.SrcCh = ed.CursorPos.Char st := textpos.Pos{ed.CursorPos.Line, 0} en := textpos.Pos{ed.CursorPos.Line, ed.CursorPos.Char} tbe := ed.Lines.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.Line] + ed.CursorPos.Char cpos := ed.charStartPos(ed.CursorPos).ToPoint() // physical location cpos.X += 5 cpos.Y += 10 ed.Complete.SrcLn = ed.CursorPos.Line ed.Complete.SrcCh = ed.CursorPos.Char ed.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.Lines == nil { return } if ed.Complete == nil { return } ed.Complete.Cancel() } // Lookup attempts to lookup symbol at current location, popping up a window // if something is found. func (ed *Editor) Lookup() { //types:add if ed.ISearch.On || ed.QReplace.On || ed.IsDisabled() { return } if ed.Complete == nil { return } var ln int var ch int if ed.HasSelection() { ln = ed.SelectRegion.Start.Line if ed.SelectRegion.End.Line != ln { return // no multiline selections for lookup } ch = ed.SelectRegion.End.Char } else { ln = ed.CursorPos.Line if ed.Lines.IsWordEnd(ed.CursorPos) { ch = ed.CursorPos.Char } else { ch = ed.Lines.WordAt(ed.CursorPos).End.Char } } ed.Complete.SrcLn = ln ed.Complete.SrcCh = ch st := textpos.Pos{ed.CursorPos.Line, 0} en := textpos.Pos{ed.CursorPos.Line, ch} tbe := ed.Lines.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.Line] + ed.CursorPos.Char cpos := ed.charStartPos(ed.CursorPos).ToPoint() // physical location cpos.X += 5 cpos.Y += 10 ed.Complete.Lookup(s, ed.CursorPos.Line, ed.CursorPos.Char, ed.Scene, cpos) } // 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, textpos.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, textpos.Pos{posLine, posChar}) if len(ld.Text) > 0 { // todo: // TextDialog(nil, "Lookup: "+txt, string(ld.Text)) return ld } if ld.Filename != "" { tx := lines.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) _ = tx _ = prmpt // todo: // 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 textcore import ( "fmt" "image" "image/draw" "cogentcore.org/core/core" "cogentcore.org/core/math32" "cogentcore.org/core/styles/states" "cogentcore.org/core/system" "cogentcore.org/core/text/textpos" ) var ( // blinker manages cursor blinking blinker = core.Blinker{} // blinkerSpriteName is the name of the window sprite used for the cursor blinkerSpriteName = "textcore.Base.Cursor" ) func init() { core.TheApp.AddQuitCleanFunc(blinker.QuitClean) blinker.Func = func() { w := blinker.Widget blinker.Unlock() // comes in locked if w == nil { return } ed := AsBase(w) if !ed.StateIs(states.Focused) || !ed.IsVisible() { ed.blinkOn = false ed.renderCursor(false) } else { // Need consistent test results on offscreen. if core.TheApp.Platform() != system.Offscreen { ed.blinkOn = !ed.blinkOn } ed.renderCursor(ed.blinkOn) } } } // startCursor starts the cursor blinking and renders it func (ed *Base) startCursor() { if ed == nil || ed.This == nil { return } if !ed.IsVisible() { return } ed.blinkOn = true ed.renderCursor(true) if core.SystemSettings.CursorBlinkTime == 0 { return } blinker.SetWidget(ed.This.(core.Widget)) blinker.Blink(core.SystemSettings.CursorBlinkTime) } // clearCursor turns off cursor and stops it from blinking func (ed *Base) clearCursor() { ed.stopCursor() ed.renderCursor(false) } // stopCursor stops the cursor from blinking func (ed *Base) stopCursor() { if ed == nil || ed.This == nil { return } blinker.ResetWidget(ed.This.(core.Widget)) } // cursorBBox returns a bounding-box for a cursor at given position func (ed *Base) cursorBBox(pos textpos.Pos) image.Rectangle { cpos := ed.charStartPos(pos) cbmin := cpos.SubScalar(ed.CursorWidth.Dots) cbmax := cpos.AddScalar(ed.CursorWidth.Dots) cbmax.Y += ed.charSize.Y curBBox := image.Rectangle{cbmin.ToPointFloor(), cbmax.ToPointCeil()} return curBBox } // renderCursor renders the cursor on or off, as a sprite that is either on or off func (ed *Base) renderCursor(on bool) { if ed == nil || ed.This == nil { return } if ed.Scene == nil || ed.Scene.Stage == nil || ed.Scene.Stage.Main == nil { return } ms := ed.Scene.Stage.Main if !on { spnm := ed.cursorSpriteName() ms.Sprites.InactivateSprite(spnm) return } if !ed.IsVisible() { return } if !ed.posIsVisible(ed.CursorPos) { 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 *Base) cursorSpriteName() string { spnm := fmt.Sprintf("%v-%v", blinkerSpriteName, ed.charSize.Y) 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 *Base) cursorSprite(on bool) *core.Sprite { sc := ed.Scene if sc == nil || sc.Stage == nil || sc.Stage.Main == 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.charSize.Y))} if bbsz.X < 2 { // at least 2 bbsz.X = 2 } sp = core.NewSprite(spnm, bbsz, image.Point{}) if ed.CursorColor != nil { ibox := sp.Pixels.Bounds() draw.Draw(sp.Pixels, ibox, ed.CursorColor, image.Point{}, draw.Src) ms.Sprites.Add(sp) } } if on { ms.Sprites.ActivateSprite(sp.Name) } else { ms.Sprites.InactivateSprite(sp.Name) } return sp } // 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 textcore 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/styles" "cogentcore.org/core/styles/states" "cogentcore.org/core/text/lines" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/token" "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 *lines.Lines, 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 := lines.FileBytes(file) if err != nil { core.ErrorDialog(ctx, err) return nil, err } bstr = lines.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 = lines.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 = lines.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.Lines.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 // [lines.Lines] for A showing the aligned edit view linesA *lines.Lines // [lines.Lines] for B showing the aligned edit view linesB *lines.Lines // aligned diffs records diff for aligned lines alignD lines.Diffs // diffs applied diffs lines.DiffSelected inInputEvent bool toolbar *core.Toolbar } func (dv *DiffEditor) Init() { dv.Frame.Init() dv.linesA = lines.NewLines() dv.linesB = lines.NewLines() dv.linesA.Settings.LineNumbers = true dv.linesB.Settings.LineNumbers = true dv.Styler(func(s *styles.Style) { s.Grow.Set(1, 1) }) f := func(name string, buf *lines.Lines) { tree.AddChildAt(dv, name, func(w *DiffTextEditor) { w.SetLines(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.linesA) f("text-b", dv.linesB) } 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.linesA.SetFilename(dv.FileA) dv.linesB.SetFilename(dv.FileB) dv.linesA.Stat() dv.linesB.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.isScrolling = true other.updateScroll(me.scrollPos) 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.Line 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(textpos.Pos{Line: df.I1}) tvb.SetCursorTarget(textpos.Pos{Line: 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.Line 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(textpos.Pos{Line: df.I1}) tvb.SetCursorTarget(textpos.Pos{Line: 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.linesA.DeleteLineColor(-1) dv.linesB.DeleteLineColor(-1) del := colors.Scheme.Error.Base ins := colors.Scheme.Success.Base chg := colors.Scheme.Primary.Base nd := len(dv.diffs.Diffs) dv.alignD = make(lines.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.linesA.SetLineColor(absln+i, chg) dv.linesB.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.linesA.SetLineColor(absln+i, ins) dv.linesB.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.linesA.SetLineColor(absln+i, del) dv.linesB.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.linesA.SetTextLines(ab) // don't copy dv.linesB.SetTextLines(bb) // don't copy dv.tagWordDiffs() dv.linesA.ReMarkup() dv.linesB.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.linesA.Line(ln) rb := dv.linesB.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 := lines.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.linesA.AddTag(ln, sla.Start, ela.End, token.TextStyleError) slb := lnb[ld.J1] elb := lnb[ld.J2-1] dv.linesB.AddTag(ln, slb.Start, elb.End, token.TextStyleError) case 'd': sla := lna[ld.I1] ela := lna[ld.I2-1] dv.linesA.AddTag(ln, sla.Start, ela.End, token.TextStyleDeleted) case 'i': slb := lnb[ld.J1] elb := lnb[ld.J2-1] dv.linesB.AddTag(ln, slb.Start, elb.End, token.TextStyleDeleted) } } } } } // applyDiff applies change from the other lines to the lines 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.Line } di, df := dv.alignD.DiffForLine(line) if di < 0 || df.Tag == 'e' { return false } if ab == 0 { dv.linesA.SetUndoOn(true) // srcLen := len(dv.BufB.Lines[df.J2]) spos := textpos.Pos{Line: df.I1, Char: 0} epos := textpos.Pos{Line: df.I2, Char: 0} src := dv.linesB.Region(spos, epos) dv.linesA.DeleteText(spos, epos) dv.linesA.InsertTextLines(spos, src.Text) // we always just copy, is blank for delete.. dv.diffs.BtoA(di) } else { dv.linesB.SetUndoOn(true) spos := textpos.Pos{Line: df.J1, Char: 0} epos := textpos.Pos{Line: df.J2, Char: 0} src := dv.linesA.Region(spos, epos) dv.linesB.DeleteText(spos, epos) dv.linesB.InsertTextLines(spos, src.Text) 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.linesA.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.linesA.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.linesB.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.linesB.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 // lines 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.LineNumberPixels()) { newPos := ed.PixelToCursor(pt) ln := newPos.Line dv := ed.diffEditor() if dv != nil && ed.Lines != 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) } // 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 textcore import ( "fmt" "unicode" "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/base/indent" "cogentcore.org/core/base/reflectx" "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/styles/abilities" "cogentcore.org/core/styles/states" "cogentcore.org/core/text/lines" "cogentcore.org/core/text/parse" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/textpos" ) // 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 // [lines.Lines] 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. // // 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 Base // ISearch is the interactive search data. ISearch ISearch `set:"-" edit:"-" json:"-" xml:"-"` // QReplace is the query replace data. QReplace QReplace `set:"-" edit:"-" json:"-" xml:"-"` // 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 // curFilename is the current filename from Lines. Used to detect changed file. curFilename string } func (ed *Editor) Init() { ed.Base.Init() ed.editorSetLines(ed.Lines) ed.setSpell() ed.AddContextMenu(ed.contextMenu) ed.handleKeyChord() ed.handleMouse() ed.handleLinkCursor() ed.handleFocus() } // UpdateNewFile checks if there is a new file in the Lines editor and updates // any relevant editor settings accordingly. func (ed *Editor) UpdateNewFile() { ln := ed.Lines if ln == nil { ed.curFilename = "" ed.viewId = -1 return } fnm := ln.Filename() if ed.curFilename == fnm { return } ed.curFilename = fnm if ln.FileInfo().Known != fileinfo.Unknown { _, ps := ln.ParseState() ed.setCompleter(ps, completeParse, completeEditParse, lookupParse) } else { ed.deleteCompleter() } } // SetLines sets the [lines.Lines] that this is an editor of, // creating a new view for this editor and connecting to events. func (ed *Editor) SetLines(ln *lines.Lines) *Editor { ed.Base.SetLines(ln) ed.editorSetLines(ln) return ed } // editorSetLines does the editor specific part of SetLines. func (ed *Editor) editorSetLines(ln *lines.Lines) { if ln == nil { ed.curFilename = "" return } ln.OnChange(ed.viewId, func(e events.Event) { ed.UpdateNewFile() }) ln.FileModPromptFunc = func() { FileModPrompt(ed.Scene, ln) } } // 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 (ed *Editor) SaveAs(filename core.Filename) { //types:add ed.editDone() SaveAs(ed.Scene, ed.Lines, string(filename), nil) } // Save saves the current text into the current filename associated with this buffer. // Do NOT use this in an OnChange event handler as it emits a Change event! Use // [Editor.SaveQuiet] instead. func (ed *Editor) Save() error { //types:add ed.editDone() return Save(ed.Scene, ed.Lines) } // SaveQuiet saves the current text into the current filename associated with this buffer. // This version does not emit a change event, so it is safe to use // in an OnChange event handler, unlike [Editor.Save]. func (ed *Editor) SaveQuiet() error { //types:add ed.clearSelected() ed.clearCursor() return Save(ed.Scene, ed.Lines) } // Close closes the lines viewed by this editor, prompting to save if there are changes. // If afterFunc is non-nil, then it is called with the status of the user action. func (ed *Editor) Close(afterFunc func(canceled bool)) bool { return Close(ed.Scene, ed.Lines, afterFunc) } 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 textpos.Region{} func (ed *Editor) shiftSelect(kt events.Event) { hasShift := kt.HasAnyModifier(key.Shift) if hasShift { if ed.SelectRegion == (textpos.Region{}) { ed.selectStart = ed.CursorPos } } else { ed.SelectRegion = textpos.Region{} } } // shiftSelectExtend updates the select region if the shift key is down and renders the selected lines. // 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) { ed.isScrolling = false if core.DebugSettings.KeyEventTrace { fmt.Printf("View KeyInput: %v\n", ed.Path()) } kf := keymap.Of(e.KeyChord()) if e.IsHandled() { return } if ed.Lines == nil || ed.Lines.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.Lines.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.cursorLineStart() ed.shiftSelectExtend(e) case keymap.End: cancelAll() e.SetHandled() ed.shiftSelect(e) ed.cursorLineEnd() 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() if ed.Lines != nil && ed.posHistoryIndex == ed.Lines.PosHistoryLen()-1 { ed.savePosHistory(ed.CursorPos) // save current if end ed.posHistoryIndex-- } 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) ed.OpenLinkAt(ed.CursorPos) case kf == keymap.FocusPrev: // tab e.SetHandled() ed.CursorPrevLink(true) ed.OpenLinkAt(ed.CursorPos) 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 = ed.Lines.MoveBackward(ed.CursorPos, 1) 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.isSpellEnabled(ed.CursorPos) { ed.offerCorrect() } else { ed.offerComplete() } case keymap.Enter: cancelAll() if !e.HasAnyModifier(key.Control, key.Meta) { e.SetHandled() if ed.Lines.Settings.AutoIndent { lp, _ := ed.Lines.ParseState() if lp != nil && lp.Lang != nil && lp.HasFlag(parse.ReAutoIndent) { // only re-indent current line for supported types tbe, _, _ := ed.Lines.AutoIndent(ed.CursorPos.Line) // reindent current line if tbe != nil { // go back to end of line! npos := textpos.Pos{Line: ed.CursorPos.Line, Char: ed.Lines.LineLen(ed.CursorPos.Line)} ed.setCursor(npos) } } ed.InsertAtCursor([]byte("\n")) tbe, _, cpos := ed.Lines.AutoIndent(ed.CursorPos.Line) if tbe != nil { ed.SetCursorShow(textpos.Pos{Line: tbe.Region.End.Line, Char: 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.Char == 0 && ed.Lines.Settings.AutoIndent { _, _, cpos := ed.Lines.AutoIndent(ed.CursorPos.Line) ed.CursorPos.Char = cpos ed.renderCursor(true) gotTabAI = true } else { ed.InsertAtCursor(indent.Bytes(ed.Lines.Settings.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.Char > 0 { ind, _ := lexer.LineIndent(ed.Lines.Line(ed.CursorPos.Line), ed.Styles.Text.TabSize) if ind > 0 { ed.Lines.IndentLine(ed.CursorPos.Line, ind-1) intxt := indent.Bytes(ed.Lines.Settings.IndentChar(), ind-1, ed.Styles.Text.TabSize) npos := textpos.Pos{Line: ed.CursorPos.Line, Char: 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.Lines.Line(pos.Line) lnLen := len(curLn) lp, ps := ed.Lines.ParseState() if lp != nil && lp.Lang != nil { match, newLine = lp.Lang.AutoBracket(ps, kt.KeyRune(), pos, curLn) } else { if kt.KeyRune() == '{' { if pos.Char == lnLen { if lnLen == 0 || unicode.IsSpace(curLn[pos.Char-1]) { newLine = true } match = true } else { match = unicode.IsSpace(curLn[pos.Char]) } } else { match = pos.Char == lnLen || unicode.IsSpace(curLn[pos.Char]) // at end or if space after } } if match { ket, _ := lexer.BracePair(kt.KeyRune()) if newLine && ed.Lines.Settings.AutoIndent { ed.InsertAtCursor([]byte(string(kt.KeyRune()) + "\n")) tbe, _, cpos := ed.Lines.AutoIndent(ed.CursorPos.Line) if tbe != nil { pos = textpos.Pos{Line: tbe.Region.End.Line, Char: cpos} ed.SetCursorShow(pos) } ed.InsertAtCursor([]byte("\n" + string(ket))) ed.Lines.AutoIndent(ed.CursorPos.Line) } else { ed.InsertAtCursor([]byte(string(kt.KeyRune()) + string(ket))) pos.Char++ } ed.lastAutoInsert = ket } else { ed.InsertAtCursor([]byte(string(kt.KeyRune()))) pos.Char++ } 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.Lines.Settings.AutoIndent && ed.CursorPos.Char == ed.Lines.LineLen(ed.CursorPos.Line) { ed.CancelComplete() ed.lastAutoInsert = 0 ed.InsertAtCursor([]byte(string(kt.KeyRune()))) tbe, _, cpos := ed.Lines.AutoIndent(ed.CursorPos.Line) if tbe != nil { ed.SetCursorShow(textpos.Pos{Line: tbe.Region.End.Line, Char: cpos}) } } else if ed.lastAutoInsert == kt.KeyRune() { // if we type what we just inserted, just move past ed.CursorPos.Char++ ed.SetCursorShow(ed.CursorPos) ed.lastAutoInsert = 0 } else { ed.lastAutoInsert = 0 ed.InsertAtCursor([]byte(string(kt.KeyRune()))) if kt.KeyRune() == ' ' { ed.CancelComplete() } else { ed.offerComplete() } } if kt.KeyRune() == '}' || kt.KeyRune() == ')' || kt.KeyRune() == ']' { cp := ed.CursorPos np := cp np.Char-- tp, found := ed.Lines.BraceMatchRune(kt.KeyRune(), np) if found { ed.addScopelights(np, tp) } } } } // handleMouse handles mouse events func (ed *Editor) handleMouse() { ed.OnClick(func(e events.Event) { ed.SetFocus() pt := ed.PointToRelPos(e.Pos()) newPos := ed.PixelToCursor(pt) if newPos == textpos.PosErr { return } switch e.MouseButton() { case events.Left: lk, _ := ed.OpenLinkAt(newPos) if lk == nil { if !e.HasAnyModifier(key.Shift, key.Meta, key.Alt) { ed.SelectReset() } ed.setCursorFromMouse(pt, newPos, e.SelectMode()) ed.savePosHistory(ed.CursorPos) } 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.Lines.LineLen(ed.CursorPos.Line) if sz > 0 { ed.SelectRegion.Start.Line = ed.CursorPos.Line ed.SelectRegion.Start.Char = 0 ed.SelectRegion.End.Line = ed.CursorPos.Line ed.SelectRegion.End.Char = sz } ed.NeedsRender() }) ed.On(events.Scroll, func(e events.Event) { ed.isScrolling = true }) ed.On(events.SlideStart, func(e events.Event) { e.SetHandled() ed.SetState(true, states.Sliding) ed.isScrolling = true pt := ed.PointToRelPos(e.Pos()) newPos := ed.PixelToCursor(pt) if ed.selectMode || e.SelectMode() != events.SelectOne { // extend existing select ed.setCursorFromMouse(pt, newPos, e.SelectMode()) } else { ed.CursorPos = newPos if !ed.selectMode { ed.selectModeToggle() } } ed.savePosHistory(ed.CursorPos) }) ed.On(events.SlideMove, func(e events.Event) { e.SetHandled() ed.selectMode = true pt := ed.PointToRelPos(e.Pos()) newPos := ed.PixelToCursor(pt) ed.setCursorFromMouse(pt, newPos, events.SelectOne) }) ed.On(events.SlideStop, func(e events.Event) { e.SetHandled() ed.isScrolling = false }) } func (ed *Editor) handleLinkCursor() { ed.On(events.MouseMove, func(e events.Event) { pt := ed.PointToRelPos(e.Pos()) newPos := ed.PixelToCursor(pt) if newPos == textpos.PosErr { return } lk, _ := ed.linkAt(newPos) if lk != nil { ed.Styles.Cursor = cursors.Pointer } else { ed.Styles.Cursor = cursors.Text } }) } //////// Context Menu // ShowContextMenu displays the context menu with options dependent on situation func (ed *Editor) ShowContextMenu(e events.Event) { if ed.spell != nil && !ed.HasSelection() && ed.isSpellEnabled(ed.CursorPos) { 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.Save).SetIcon(icons.Save) core.NewFuncButton(m).SetFunc(ed.SaveAs).SetIcon(icons.SaveAs) core.NewFuncButton(m).SetFunc(ed.Lines.Open).SetIcon(icons.Open) core.NewFuncButton(m).SetFunc(ed.Lines.Revert).SetIcon(icons.Reset) } else { core.NewButton(m).SetText("Clear").SetIcon(icons.ClearAll). OnClick(func(e events.Event) { ed.Clear() }) if ed.Lines != nil && ed.Lines.FileInfo().Generated { core.NewButton(m).SetText("Set editable").SetIcon(icons.Edit). OnClick(func(e events.Event) { ed.SetReadOnly(false) ed.Lines.FileInfo().Generated = false ed.Update() }) } } } // 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(textpos.Pos{Line: ln - 1}) ed.savePosHistory(ed.CursorPos) } // 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 textcore import ( "fmt" "os" "time" "cogentcore.org/core/base/errors" "cogentcore.org/core/base/fsx" "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/text/lines" ) // SaveAs saves the given Lines text into the given file. // 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 SaveAs(sc *core.Scene, lns *lines.Lines, filename string, afterFunc func(canceled bool)) { if !errors.Log1(fsx.FileExists(filename)) { lns.SaveFile(filename) if afterFunc != nil { afterFunc(false) } } else { 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) { lns.SaveFile(filename) if afterFunc != nil { afterFunc(false) } }) }) d.RunDialog(sc) } } // Save saves the given Lines into the current filename associated with this buffer, // prompting if the file is changed on disk since the last save. func Save(sc *core.Scene, lns *lines.Lines) error { fname := lns.Filename() if fname == "" { return errors.New("core.Editor: filename is empty for Save") } info, err := os.Stat(fname) if err == nil && info.ModTime() != time.Time(lns.FileInfo().ModTime) { 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", fname)) d.AddBottomBar(func(bar *core.Frame) { core.NewButton(bar).SetText("Save to different file").OnClick(func(e events.Event) { d.Close() fd := core.NewBody("Save file as") fv := core.NewFilePicker(fd).SetFilename(fname) fv.OnSelect(func(e events.Event) { SaveAs(sc, lns, fv.SelectedFile(), nil) }) fd.RunWindowDialog(sc) }) core.NewButton(bar).SetText("Open from disk, losing changes").OnClick(func(e events.Event) { d.Close() lns.Revert() }) core.NewButton(bar).SetText("Save file, overwriting").OnClick(func(e events.Event) { d.Close() lns.SaveFile(fname) }) }) d.RunDialog(sc) } return lns.SaveFile(fname) } // Close closes the lines viewed by this editor, prompting to save if there are changes. // If afterFunc is non-nil, then it is called with the status of the user action. // Returns false if the file was actually not closed pending input from the user. func Close(sc *core.Scene, lns *lines.Lines, afterFunc func(canceled bool)) bool { if !lns.IsNotSaved() { lns.Close() if afterFunc != nil { afterFunc(false) } return true } lns.StopDelayedReMarkup() fname := lns.Filename() if fname == "" { 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 afterFunc != nil { afterFunc(true) } }) d.AddOK(bar).SetText("Close without saving").OnClick(func(e events.Event) { lns.ClearNotSaved() lns.AutosaveDelete() Close(sc, lns, afterFunc) }) }) d.RunDialog(sc) return false // awaiting decisions.. } 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?", fname)) d.AddBottomBar(func(bar *core.Frame) { core.NewButton(bar).SetText("Cancel").OnClick(func(e events.Event) { d.Close() if afterFunc != nil { afterFunc(true) } }) core.NewButton(bar).SetText("Close without saving").OnClick(func(e events.Event) { d.Close() lns.ClearNotSaved() lns.AutosaveDelete() Close(sc, lns, afterFunc) }) core.NewButton(bar).SetText("Save").OnClick(func(e events.Event) { d.Close() Save(sc, lns) Close(sc, lns, afterFunc) // 2nd time through won't prompt }) }) d.RunDialog(sc) return false } // FileModPrompt is called when a file has been modified in the filesystem // and it is about to be modified through an edit, in the fileModCheck function. // The prompt determines whether the user wants to revert, overwrite, or // save current version as a different file. func FileModPrompt(sc *core.Scene, lns *lines.Lines) bool { fname := lns.Filename() d := core.NewBody("File changed on disk: " + fsx.DirAndFile(fname)) 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", fname)) d.AddBottomBar(func(bar *core.Frame) { core.NewButton(bar).SetText("Save as to different file").OnClick(func(e events.Event) { d.Close() fd := core.NewBody("Save file as") fv := core.NewFilePicker(fd).SetFilename(fname) fv.OnSelect(func(e events.Event) { SaveAs(sc, lns, fv.SelectedFile(), nil) }) fd.RunWindowDialog(sc) }) core.NewButton(bar).SetText("Revert from disk").OnClick(func(e events.Event) { d.Close() lns.Revert() }) core.NewButton(bar).SetText("Ignore and proceed").OnClick(func(e events.Event) { d.Close() lns.SetFileModOK(true) }) }) d.RunDialog(sc) 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 textcore import ( "unicode" "cogentcore.org/core/base/stringsx" "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/styles" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/textpos" ) // 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) ([]textpos.Match, bool) { fsz := len(find) if fsz == 0 { ed.Highlights = nil return nil, false } _, matches := ed.Lines.Search([]byte(find), !useCase, lexItems) if len(matches) == 0 { ed.Highlights = nil return matches, false } hi := make([]textpos.Region, len(matches)) for i, m := range matches { hi[i] = m.Region 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 []textpos.Match, cpos textpos.Pos) (int, bool) { for i, m := range matches { reg := ed.Lines.AdjustRegion(m.Region) 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 []textpos.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 textpos.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 textpos.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.Lines.AdjustRegion(m.Region) 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 []textpos.Match `json:"-" xml:"-"` // position within isearch matches pos int `json:"-" xml:"-"` // starting position for search -- returns there after on cancel startPos textpos.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.Lines.AdjustRegion(m.Region) 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.Lines.AdjustRegion(m.Region) 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.Lines.ReplaceText(reg.Start, reg.End, pos, rep, matchCase) ed.Highlights[midx] = textpos.Region{} 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() } // 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 textcore import ( "fmt" "cogentcore.org/core/core" "cogentcore.org/core/math32" "cogentcore.org/core/styles" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/textpos" ) // maxGrowLines is the maximum number of lines to grow to // (subject to other styling constraints). const maxGrowLines = 25 // styleSizes gets the charSize based on Style settings, // and updates lineNumberOffset. func (ed *Base) styleSizes() { ed.lineNumberDigits = max(1+int(math32.Log10(float32(ed.NumLines()))), 3) sty, tsty := ed.Styles.NewRichText() lno := true if ed.Lines != nil { lno = ed.Lines.Settings.LineNumbers ed.Lines.SetFontStyle(sty) } if lno { ed.hasLineNumbers = true ed.lineNumberOffset = ed.lineNumberDigits + 2 } else { ed.hasLineNumbers = false ed.lineNumberOffset = 0 } if ed.Scene == nil { ed.charSize.Set(16, 22) return } sh := ed.Scene.TextShaper() if sh != nil { lht := ed.Styles.LineHeightDots() tx := rich.NewText(sty, []rune{'M'}) r := sh.Shape(tx, tsty, &rich.DefaultSettings) ed.charSize.X = math32.Round(r[0].Advance()) ed.charSize.Y = lht } } // visSizeFromAlloc updates visSize based on allocated size. func (ed *Base) visSizeFromAlloc() { asz := ed.Geom.Size.Alloc.Content sbw := math32.Ceil(ed.Styles.ScrollbarWidth.Dots) if ed.HasScroll[math32.Y] { asz.X -= sbw } if ed.HasScroll[math32.X] { asz.Y -= sbw } ed.visSizeAlloc = asz ed.visSize.Y = int(math32.Floor(float32(asz.Y) / ed.charSize.Y)) ed.visSize.X = int(math32.Floor(float32(asz.X) / ed.charSize.X)) // fmt.Println("vis size:", ed.visSize, "alloc:", asz, "charSize:", ed.charSize, "grow:", sty.Grow) } // layoutAllLines uses the visSize width to update the line wrapping // of the Lines text, getting the total height. func (ed *Base) layoutAllLines() { ed.visSizeFromAlloc() if ed.visSize.Y == 0 || ed.Lines == nil || ed.Lines.NumLines() == 0 { return } ed.lastFilename = ed.Lines.Filename() sty := &ed.Styles buf := ed.Lines // todo: self-lock method for this, and general better api buf.Highlighter.TabSize = sty.Text.TabSize // todo: may want to support horizontal scroll and min width ed.linesSize.X = ed.visSize.X - ed.lineNumberOffset // width buf.SetWidth(ed.viewId, ed.linesSize.X) // inexpensive if same, does update ed.linesSize.Y = buf.ViewLines(ed.viewId) ed.totalSize.X = ed.charSize.X * float32(ed.visSize.X) ed.totalSize.Y = ed.charSize.Y * float32(ed.linesSize.Y) ed.lineRenders = make([]renderCache, ed.visSize.Y+1) ed.lineNoRenders = make([]renderCache, ed.visSize.Y+1) // ed.hasLinks = false // todo: put on lines ed.lastVisSizeAlloc = ed.visSizeAlloc } // reLayoutAllLines updates the Renders Layout given current size, if changed func (ed *Base) reLayoutAllLines() { ed.visSizeFromAlloc() if ed.visSize.Y == 0 || ed.Lines == nil || ed.Lines.NumLines() == 0 { return } if ed.lastVisSizeAlloc == ed.visSizeAlloc { return } ed.layoutAllLines() } // note: Layout reverts to basic Widget behavior for layout if no kids, like us.. // sizeToLines sets the Actual.Content size based on number of lines of text, // subject to maxGrowLines, for the non-grow case. func (ed *Base) sizeToLines() { if ed.Styles.Grow.Y > 0 { return } nln := ed.Lines.ViewLines(ed.viewId) // if ed.linesSize.Y > 0 { // we have already been through layout // nln = ed.linesSize.Y // } nln = min(maxGrowLines, nln) maxh := float32(nln) * ed.charSize.Y sz := &ed.Geom.Size ty := styles.ClampMin(styles.ClampMax(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, "textcore.Base sizeToLines targ:", ty, "nln:", nln, "Actual:", sz.Actual.Content) } } func (ed *Base) SizeUp() { ed.Frame.SizeUp() // sets Actual size based on styles if ed.Lines == nil || ed.Lines.NumLines() == 0 { return } ed.sizeToLines() } func (ed *Base) SizeDown(iter int) bool { if iter == 0 { if ed.NeedsRebuild() && ed.Lines != nil { ed.Lines.ReMarkup() } ed.layoutAllLines() } else { ed.reLayoutAllLines() } ed.sizeToLines() redo := ed.Frame.SizeDown(iter) chg := ed.ManageOverflow(iter, true) if !ed.HasScroll[math32.Y] { ed.scrollPos = 0 } return redo || chg } func (ed *Base) SizeFinal() { ed.Frame.SizeFinal() ed.reLayoutAllLines() } func (ed *Base) Position() { ed.Frame.Position() ed.ConfigScrolls() } func (ed *Base) ApplyScenePos() { ed.Frame.ApplyScenePos() ed.PositionScrolls() } func (ed *Base) ScrollValues(d math32.Dims) (maxSize, visSize, visPct float32) { if d == math32.X { return ed.Frame.ScrollValues(d) } maxSize = float32(max(ed.linesSize.Y, 1)) * ed.charSize.Y visSize = float32(ed.visSize.Y) * ed.charSize.Y visPct = visSize / maxSize // fmt.Println("scroll values:", maxSize, visSize, visPct) return } func (ed *Base) ScrollChanged(d math32.Dims, sb *core.Slider) { ed.isScrolling = true if d == math32.X { ed.Frame.ScrollChanged(d, sb) return } ed.scrollPos = sb.Value / ed.charSize.Y ed.lineRenders = make([]renderCache, ed.visSize.Y+1) ed.lineNoRenders = make([]renderCache, ed.visSize.Y+1) ed.NeedsRender() } func (ed *Base) SetScrollParams(d math32.Dims, sb *core.Slider) { if d == math32.X { ed.Frame.SetScrollParams(d, sb) return } sb.Min = 0 sb.Step = 1 if ed.visSize.Y > 0 { sb.PageStep = float32(ed.visSize.Y) * ed.charSize.Y } } // updateScroll sets the scroll position to given value, in lines. // calls a NeedsRender if changed. func (ed *Base) updateScroll(pos float32) bool { if !ed.HasScroll[math32.Y] || ed.Scrolls[math32.Y] == nil { return false } if pos < 0 { pos = 0 } ed.scrollPos = pos ppos := pos * ed.charSize.Y sb := ed.Scrolls[math32.Y] if sb.Value != ppos { ed.isScrolling = true sb.SetValue(ppos) ed.NeedsRender() return true } return false } //////// Scrolling -- Vertical // scrollLineToTop positions scroll so that the line of given source position // is at the top (to the extent possible). func (ed *Base) scrollLineToTop(pos textpos.Pos) bool { vp := ed.Lines.PosToView(ed.viewId, pos) return ed.updateScroll(float32(vp.Line)) } // scrollCursorToTop positions scroll so the cursor line is at the top. func (ed *Base) scrollCursorToTop() bool { return ed.scrollLineToTop(ed.CursorPos) } // scrollLineToBottom positions scroll so that the line of given source position // is at the bottom (to the extent possible). func (ed *Base) scrollLineToBottom(pos textpos.Pos) bool { vp := ed.Lines.PosToView(ed.viewId, pos) return ed.updateScroll(float32(vp.Line - ed.visSize.Y + 1)) } // scrollCursorToBottom positions scroll so cursor line is at the bottom. func (ed *Base) scrollCursorToBottom() bool { return ed.scrollLineToBottom(ed.CursorPos) } // scrollLineToCenter positions scroll so that the line of given source position // is at the center (to the extent possible). func (ed *Base) scrollLineToCenter(pos textpos.Pos) bool { vp := ed.Lines.PosToView(ed.viewId, pos) return ed.updateScroll(float32(max(vp.Line-ed.visSize.Y/2, 0))) } func (ed *Base) scrollCursorToCenter() bool { return ed.scrollLineToCenter(ed.CursorPos) } func (ed *Base) scrollCursorToTarget() { // fmt.Println(ed, "to target:", ed.CursorTarg) ed.targetSet = false if ed.cursorTarget == textpos.PosErr { ed.cursorEndDoc() return } ed.CursorPos = ed.cursorTarget ed.scrollCursorToCenter() } // scrollToCenterIfHidden checks if the given position is not in view, // and scrolls to center if so. returns false if in view already. func (ed *Base) scrollToCenterIfHidden(pos textpos.Pos) bool { if ed.Lines == nil { return false } vp := ed.Lines.PosToView(ed.viewId, pos) spos := ed.Geom.ContentBBox.Min.X + int(ed.LineNumberPixels()) epos := ed.Geom.ContentBBox.Max.X csp := ed.charStartPos(pos).ToPoint() if vp.Line >= int(ed.scrollPos) && vp.Line < int(ed.scrollPos)+ed.visSize.Y { if csp.X >= spos && csp.X < epos { return false } } else { ed.scrollLineToCenter(pos) } if csp.X < spos { ed.scrollCursorToRight() } else if csp.X > epos { // ed.scrollCursorToLeft() } return true } // scrollCursorToCenterIfHidden checks if the cursor position is not in view, // and scrolls to center if so. returns false if in view already. func (ed *Base) scrollCursorToCenterIfHidden() bool { return ed.scrollToCenterIfHidden(ed.CursorPos) } //////// 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 *Base) 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 *Base) scrollCursorToRight() bool { curBBox := ed.cursorBBox(ed.CursorPos) return ed.scrollToRight(curBBox.Max.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 textcore import ( "cogentcore.org/core/system" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/textpos" ) // openLink opens given link, using the LinkHandler if non-nil, // or the default system.TheApp.OpenURL() which will open a browser. func (ed *Base) openLink(tl *rich.Hyperlink) { if ed.LinkHandler != nil { ed.LinkHandler(tl) } else { system.TheApp.OpenURL(tl.URL) } } // linkAt returns hyperlink at given source position, if one exists there, // otherwise returns nil. func (ed *Base) linkAt(pos textpos.Pos) (*rich.Hyperlink, int) { lk := ed.Lines.LinkAt(pos) if lk == nil { return nil, -1 } return lk, pos.Line } // OpenLinkAt opens a link at given cursor position, if one exists there. // returns the link if found, else nil. Also highlights the selected link. func (ed *Base) OpenLinkAt(pos textpos.Pos) (*rich.Hyperlink, int) { tl, ln := ed.linkAt(pos) if tl == nil { return nil, -1 } ed.HighlightsReset() reg := ed.highlightLink(tl, ln) ed.SetCursorShow(reg.Start) ed.openLink(tl) return tl, pos.Line } // highlightLink highlights given hyperlink func (ed *Base) highlightLink(lk *rich.Hyperlink, ln int) textpos.Region { reg := textpos.NewRegion(ln, lk.Range.Start, ln, lk.Range.End) ed.HighlightRegion(reg) return reg } // CursorNextLink moves cursor to next link. wraparound wraps around to top of // buffer if none found -- returns true if found func (ed *Base) CursorNextLink(wraparound bool) bool { if ed.NumLines() == 0 { return false } ed.validateCursor() nl, ln := ed.Lines.NextLink(ed.CursorPos) if nl == nil { if !wraparound { return false } nl, ln = ed.Lines.NextLink(textpos.Pos{}) // wraparound if nl == nil { return false } } ed.HighlightsReset() reg := ed.highlightLink(nl, ln) ed.SetCursorTarget(reg.Start) ed.savePosHistory(reg.Start) ed.NeedsRender() return true } // CursorPrevLink moves cursor to next link. wraparound wraps around to bottom of // buffer if none found -- returns true if found func (ed *Base) CursorPrevLink(wraparound bool) bool { if ed.NumLines() == 0 { return false } ed.validateCursor() nl, ln := ed.Lines.PrevLink(ed.CursorPos) if nl == nil { if !wraparound { return false } nl, ln = ed.Lines.PrevLink(ed.Lines.EndPos()) // wraparound if nl == nil { return false } } ed.HighlightsReset() reg := ed.highlightLink(nl, ln) ed.SetCursorTarget(reg.Start) ed.savePosHistory(reg.Start) ed.NeedsRender() 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 textcore import ( "image" "cogentcore.org/core/events" "cogentcore.org/core/math32" "cogentcore.org/core/styles/states" "cogentcore.org/core/text/textpos" ) // validateCursor sets current cursor to a valid cursor position func (ed *Base) validateCursor() textpos.Pos { if ed.Lines != nil { ed.CursorPos = ed.Lines.ValidPos(ed.CursorPos) } else { ed.CursorPos = textpos.Pos{} } return ed.CursorPos } // setCursor sets a new cursor position, enforcing it in range. // This is the main final pathway for all cursor movement. func (ed *Base) setCursor(pos textpos.Pos) { if ed.Lines == nil { ed.CursorPos = textpos.PosZero return } ed.scopelightsReset() ed.CursorPos = ed.Lines.ValidPos(pos) bm, has := ed.Lines.BraceMatch(pos) if has { ed.addScopelights(pos, bm) ed.NeedsRender() } } // SetCursorShow sets a new cursor position, enforcing it in range, and shows // the cursor (scroll to if hidden, render) func (ed *Base) SetCursorShow(pos textpos.Pos) { ed.setCursor(pos) ed.scrollCursorToCenterIfHidden() ed.renderCursor(true) } // SetCursorTarget sets a new cursor target position, ensures that it is visible. // Setting the textpos.PosErr value causes it to go the end of doc, the position // of which may not be known at the time the target is set. func (ed *Base) SetCursorTarget(pos textpos.Pos) { ed.isScrolling = false ed.targetSet = true ed.cursorTarget = pos if pos == textpos.PosErr { ed.cursorEndDoc() return } ed.SetCursorShow(pos) // fmt.Println(ed, "set target:", ed.CursorTarg) } // 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 (ed *Base) savePosHistory(pos textpos.Pos) bool { if ed.Lines == nil { return false } did := ed.Lines.PosHistorySave(pos) if did { ed.posHistoryIndex = ed.Lines.PosHistoryLen() - 1 } return did } // CursorToHistoryPrev moves cursor to previous position on history list. // returns true if moved func (ed *Base) CursorToHistoryPrev() bool { if ed.Lines == nil { ed.CursorPos = textpos.Pos{} return false } sz := ed.Lines.PosHistoryLen() if sz == 0 { return false } if ed.posHistoryIndex < 0 { ed.posHistoryIndex = 0 return false } ed.posHistoryIndex = min(sz-1, ed.posHistoryIndex) pos, _ := ed.Lines.PosHistoryAt(ed.posHistoryIndex) ed.CursorPos = ed.Lines.ValidPos(pos) if ed.posHistoryIndex > 0 { ed.posHistoryIndex-- } ed.scrollCursorToCenterIfHidden() ed.renderCursor(true) ed.SendInput() return true } // CursorToHistoryNext moves cursor to previous position on history list -- // returns true if moved func (ed *Base) CursorToHistoryNext() bool { if ed.Lines == nil { ed.CursorPos = textpos.Pos{} return false } sz := ed.Lines.PosHistoryLen() if sz == 0 { return false } ed.posHistoryIndex++ if ed.posHistoryIndex >= sz-1 { ed.posHistoryIndex = sz - 1 } pos, _ := ed.Lines.PosHistoryAt(ed.posHistoryIndex) ed.CursorPos = ed.Lines.ValidPos(pos) ed.scrollCursorToCenterIfHidden() ed.renderCursor(true) ed.SendInput() return true } // setCursorColumn sets the current target cursor column (cursorColumn) to that // of the given position func (ed *Base) setCursorColumn(pos textpos.Pos) { if ed.Lines == nil { return } vpos := ed.Lines.PosToView(ed.viewId, pos) ed.cursorColumn = vpos.Char } //////// cursor moving // cursorSelect updates selection based on cursor movements, given starting // cursor position and ed.CursorPos is current func (ed *Base) cursorSelect(org textpos.Pos) { if !ed.selectMode { return } ed.selectRegionUpdate(ed.CursorPos) } // cursorSelectShow does SetCursorShow, cursorSelect, and NeedsRender. // This is typically called for move actions. func (ed *Base) cursorSelectShow(org textpos.Pos) { ed.SetCursorShow(ed.CursorPos) ed.cursorSelect(org) ed.SendInput() ed.NeedsRender() } // cursorForward moves the cursor forward func (ed *Base) cursorForward(steps int) { org := ed.validateCursor() ed.CursorPos = ed.Lines.MoveForward(org, steps) ed.setCursorColumn(ed.CursorPos) ed.cursorSelectShow(org) } // cursorForwardWord moves the cursor forward by words func (ed *Base) cursorForwardWord(steps int) { org := ed.validateCursor() ed.CursorPos = ed.Lines.MoveForwardWord(org, steps) ed.setCursorColumn(ed.CursorPos) ed.cursorSelectShow(org) } // cursorBackward moves the cursor backward func (ed *Base) cursorBackward(steps int) { org := ed.validateCursor() ed.CursorPos = ed.Lines.MoveBackward(org, steps) ed.setCursorColumn(ed.CursorPos) ed.cursorSelectShow(org) } // cursorBackwardWord moves the cursor backward by words func (ed *Base) cursorBackwardWord(steps int) { org := ed.validateCursor() ed.CursorPos = ed.Lines.MoveBackwardWord(org, steps) ed.setCursorColumn(ed.CursorPos) ed.cursorSelectShow(org) } // cursorDown moves the cursor down line(s) func (ed *Base) cursorDown(steps int) { org := ed.validateCursor() ed.CursorPos = ed.Lines.MoveDown(ed.viewId, org, steps, ed.cursorColumn) ed.cursorSelectShow(org) } // cursorPageDown moves the cursor down page(s), where a page is defined // dynamically as just moving the cursor off the screen func (ed *Base) cursorPageDown(steps int) { org := ed.validateCursor() vp := ed.Lines.PosToView(ed.viewId, ed.CursorPos) cpr := max(0, vp.Line-int(ed.scrollPos)) nln := max(1, ed.visSize.Y-cpr) for range steps { ed.CursorPos = ed.Lines.MoveDown(ed.viewId, ed.CursorPos, nln, ed.cursorColumn) } ed.setCursor(ed.CursorPos) ed.scrollCursorToTop() ed.renderCursor(true) ed.cursorSelect(org) ed.SendInput() ed.NeedsRender() } // cursorUp moves the cursor up line(s) func (ed *Base) cursorUp(steps int) { org := ed.validateCursor() ed.CursorPos = ed.Lines.MoveUp(ed.viewId, org, steps, ed.cursorColumn) ed.cursorSelectShow(org) } // cursorPageUp moves the cursor up page(s), where a page is defined // dynamically as just moving the cursor off the screen func (ed *Base) cursorPageUp(steps int) { org := ed.validateCursor() vp := ed.Lines.PosToView(ed.viewId, ed.CursorPos) cpr := max(0, vp.Line-int(ed.scrollPos)) nln := max(1, cpr) for range steps { ed.CursorPos = ed.Lines.MoveUp(ed.viewId, ed.CursorPos, nln, ed.cursorColumn) } ed.setCursor(ed.CursorPos) ed.scrollCursorToBottom() ed.renderCursor(true) ed.cursorSelect(org) ed.SendInput() ed.NeedsRender() } // cursorRecenter re-centers the view around the cursor position, toggling // between putting cursor in middle, top, and bottom of view func (ed *Base) cursorRecenter() { ed.validateCursor() ed.savePosHistory(ed.CursorPos) cur := (ed.lastRecenter + 1) % 3 switch cur { case 0: ed.scrollCursorToBottom() case 1: ed.scrollCursorToCenter() case 2: ed.scrollCursorToTop() } ed.lastRecenter = cur } // cursorLineStart moves the cursor to the start of the line, updating selection // if select mode is active func (ed *Base) cursorLineStart() { org := ed.validateCursor() ed.CursorPos = ed.Lines.MoveLineStart(ed.viewId, org) ed.cursorColumn = 0 ed.scrollCursorToRight() ed.cursorSelectShow(org) } // CursorStartDoc moves the cursor to the start of the text, updating selection // if select mode is active func (ed *Base) CursorStartDoc() { org := ed.validateCursor() ed.CursorPos.Line = 0 ed.CursorPos.Char = 0 ed.cursorColumn = 0 ed.scrollCursorToTop() ed.cursorSelectShow(org) } // cursorLineEnd moves the cursor to the end of the text func (ed *Base) cursorLineEnd() { org := ed.validateCursor() ed.CursorPos = ed.Lines.MoveLineEnd(ed.viewId, org) ed.setCursorColumn(ed.CursorPos) ed.scrollCursorToRight() ed.cursorSelectShow(org) } // cursorEndDoc moves the cursor to the end of the text, updating selection if // select mode is active func (ed *Base) cursorEndDoc() { org := ed.validateCursor() ed.CursorPos = ed.Lines.EndPos() ed.setCursorColumn(ed.CursorPos) ed.scrollCursorToBottom() ed.cursorSelectShow(org) } // todo: ctrl+backspace = delete word // shift+arrow = select // uparrow = start / down = end // cursorBackspace deletes character(s) immediately before cursor func (ed *Base) cursorBackspace(steps int) { org := ed.validateCursor() 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.Lines.DeleteText(ed.CursorPos, org) ed.NeedsRender() } // cursorDelete deletes character(s) immediately after the cursor func (ed *Base) cursorDelete(steps int) { org := ed.validateCursor() if ed.HasSelection() { ed.deleteSelection() return } // note: no update b/c signal from buf will drive update ed.cursorForward(steps) ed.Lines.DeleteText(org, ed.CursorPos) ed.SetCursorShow(org) ed.NeedsRender() } // cursorBackspaceWord deletes words(s) immediately before cursor func (ed *Base) cursorBackspaceWord(steps int) { org := ed.validateCursor() if ed.HasSelection() { ed.deleteSelection() ed.SetCursorShow(org) return } ed.cursorBackwardWord(steps) ed.scrollCursorToCenterIfHidden() ed.renderCursor(true) ed.Lines.DeleteText(ed.CursorPos, org) ed.NeedsRender() } // cursorDeleteWord deletes word(s) immediately after the cursor func (ed *Base) cursorDeleteWord(steps int) { org := ed.validateCursor() if ed.HasSelection() { ed.deleteSelection() return } ed.cursorForwardWord(steps) ed.Lines.DeleteText(org, ed.CursorPos) ed.SetCursorShow(org) ed.NeedsRender() } // cursorKill deletes text from cursor to end of text. // if line is empty, deletes the line. func (ed *Base) cursorKill() { org := ed.validateCursor() llen := ed.Lines.LineLen(ed.CursorPos.Line) if ed.CursorPos.Char == llen { // at end ed.cursorForward(1) } else { ed.cursorLineEnd() } ed.Lines.DeleteText(org, ed.CursorPos) ed.SetCursorShow(org) ed.NeedsRender() } // cursorTranspose swaps the character at the cursor with the one before it. func (ed *Base) cursorTranspose() { ed.validateCursor() pos := ed.CursorPos if pos.Char == 0 { return } ed.Lines.TransposeChar(ed.viewId, pos) // ed.SetCursorShow(pos) ed.NeedsRender() } // cursorTranspose swaps the character at the cursor with the one before it func (ed *Base) cursorTransposeWord() { // todo: } // setCursorFromMouse sets cursor position from mouse mouse action -- handles // the selection updating etc. func (ed *Base) setCursorFromMouse(pt image.Point, newPos textpos.Pos, selMode events.SelectModes) { oldPos := ed.CursorPos if newPos == oldPos || newPos == textpos.PosErr { return } // fmt.Printf("set cursor fm mouse: %v\n", newPos) defer ed.NeedsRender() if !ed.selectMode && selMode == events.ExtendContinuous { if ed.SelectRegion == (textpos.Region{}) { 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.Line ch := ed.CursorPos.Char if ln != ed.SelectRegion.Start.Line || ch < ed.SelectRegion.Start.Char || ch > ed.SelectRegion.End.Char { ed.SelectReset() } } else { ed.selectRegionUpdate(ed.CursorPos) } if ed.StateIs(states.Sliding) { scPos := math32.FromPoint(pt) // already relative to editor ed.AutoScroll(scPos) } else { ed.scrollCursorToCenterIfHidden() } } else if ed.HasSelection() { ln := ed.CursorPos.Line ch := ed.CursorPos.Char if ln != ed.SelectRegion.Start.Line || ch < ed.SelectRegion.Start.Char || ch > ed.SelectRegion.End.Char { ed.SelectReset() } } } // 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 textcore import ( "bufio" "io" "sync" "time" "cogentcore.org/core/text/lines" "cogentcore.org/core/text/rich" ) // OutputBufferMarkupFunc is a function that returns a marked-up version // of a given line of output text. It is essential that it not add any // new text, just splits into spans with different styles. type OutputBufferMarkupFunc func(buf *lines.Lines, line []rune) rich.Text // 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 text. type OutputBuffer struct { //types:add -setters // the output that we are reading from, as an io.Reader Output io.Reader // the [lines.Lines] that we output to Lines *lines.Lines // how much time to wait while batching output (default: 200ms) Batch time.Duration // MarkupFunc is an optional markup function that adds html tags to given line // of output. It is essential that it not add any new text, just splits into spans // with different styles. MarkupFunc OutputBufferMarkupFunc // current buffered output raw lines, which are not yet sent to the Buffer bufferedLines [][]rune // current buffered output markup lines, which are not yet sent to the Buffer bufferedMarkup []rich.Text // 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 // mutex protecting updates sync.Mutex } // MonitorOutput monitors the output and updates the [Buffer]. func (ob *OutputBuffer) MonitorOutput() { if ob.Batch == 0 { ob.Batch = 200 * time.Millisecond } sty := ob.Lines.FontStyle() ob.bufferedLines = make([][]rune, 0, 100) ob.bufferedMarkup = make([]rich.Text, 0, 100) outscan := bufio.NewScanner(ob.Output) // line at a time for outscan.Scan() { ob.Lock() b := outscan.Bytes() rln := []rune(string(b)) if ob.afterTimer != nil { ob.afterTimer.Stop() ob.afterTimer = nil } ob.bufferedLines = append(ob.bufferedLines, rln) if ob.MarkupFunc != nil { mup := ob.MarkupFunc(ob.Lines, rln) ob.bufferedMarkup = append(ob.bufferedMarkup, mup) } else { mup := rich.NewText(sty, rln) ob.bufferedMarkup = append(ob.bufferedMarkup, 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.Lock() ob.lastOutput = time.Now() ob.outputToBuffer() ob.afterTimer = nil ob.Unlock() }) } ob.Unlock() } ob.Lock() ob.outputToBuffer() ob.Unlock() } // outputToBuffer sends the current output to Buffer. // MUST be called under mutex protection func (ob *OutputBuffer) outputToBuffer() { if len(ob.bufferedLines) == 0 { return } ob.Lines.SetUndoOn(false) ob.Lines.AppendTextMarkup(ob.bufferedLines, ob.bufferedMarkup) ob.bufferedLines = make([][]rune, 0, 100) ob.bufferedMarkup = make([]rich.Text, 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 textcore import ( "fmt" "image" "image/color" "slices" "cogentcore.org/core/colors" "cogentcore.org/core/colors/gradient" "cogentcore.org/core/colors/matcolor" "cogentcore.org/core/math32" "cogentcore.org/core/paint/render" "cogentcore.org/core/styles/sides" "cogentcore.org/core/styles/states" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/textpos" ) func (ed *Base) reLayout() { if ed.Lines == nil { return } prevLines := ed.linesSize.Y lns := ed.Lines.ViewLines(ed.viewId) if lns == prevLines { return } ed.layoutAllLines() chg := ed.ManageOverflow(1, true) if ed.Styles.Grow.Y == 0 && lns < maxGrowLines || prevLines < maxGrowLines { chg = prevLines != ed.linesSize.Y || chg } if chg { // fmt.Println(chg, lns, prevLines, ed.visSize.Y, ed.linesSize.Y) ed.NeedsLayout() if !ed.HasScroll[math32.Y] { ed.scrollPos = 0 } } } func (ed *Base) RenderWidget() { if ed.StartRender() { ed.reLayout() if ed.targetSet { ed.scrollCursorToTarget() } if !ed.isScrolling { ed.scrollCursorToCenterIfHidden() } ed.PositionScrolls() ed.renderLines() if ed.StateIs(states.Focused) { ed.startCursor() } else { ed.stopCursor() } ed.RenderChildren() ed.RenderScrolls() ed.EndRender() } else { ed.stopCursor() } } // renderBBox is the bounding box for the text render area (ContentBBox) func (ed *Base) renderBBox() image.Rectangle { return ed.Geom.ContentBBox } // renderLineStartEnd returns the starting and ending (inclusive) lines to render // based on the scroll position. Also returns the starting upper left position // for rendering the first line. func (ed *Base) renderLineStartEnd() (stln, edln int, spos math32.Vector2) { spos = ed.Geom.Pos.Content stln = int(math32.Floor(ed.scrollPos)) spos.Y += (float32(stln) - ed.scrollPos) * ed.charSize.Y edln = min(ed.linesSize.Y-1, stln+ed.visSize.Y) return } // posIsVisible returns true if given position is visible, // in terms of the vertical lines in view. func (ed *Base) posIsVisible(pos textpos.Pos) bool { if ed.Lines == nil { return false } vpos := ed.Lines.PosToView(ed.viewId, pos) sp := int(math32.Floor(ed.scrollPos)) return vpos.Line >= sp && vpos.Line <= sp+ed.visSize.Y } // renderLines renders the visible lines and line numbers. func (ed *Base) renderLines() { ed.RenderStandardBox() if ed.Lines == nil { return } bb := ed.renderBBox() stln, edln, spos := ed.renderLineStartEnd() pc := &ed.Scene.Painter pc.PushContext(nil, render.NewBoundsRect(bb, sides.NewFloats())) if ed.hasLineNumbers { ed.renderLineNumbersBox() li := 0 lastln := -1 for ln := stln; ln <= edln; ln++ { sp := ed.Lines.PosFromView(ed.viewId, textpos.Pos{Line: ln}) if sp.Char == 0 && sp.Line != lastln { // Char=0 is start of source line // but also get 0 for out-of-range.. ed.renderLineNumber(spos, li, sp.Line) lastln = sp.Line } li++ } } ed.renderDepthBackground(spos, stln, edln) if ed.hasLineNumbers { tbb := bb tbb.Min.X += int(ed.LineNumberPixels()) pc.PushContext(nil, render.NewBoundsRect(tbb, sides.NewFloats())) } buf := ed.Lines rpos := spos rpos.X += ed.LineNumberPixels() vsel := buf.RegionToView(ed.viewId, ed.SelectRegion) rtoview := func(rs []textpos.Region) []textpos.Region { if len(rs) == 0 { return nil } hlts := make([]textpos.Region, 0, len(rs)) for _, reg := range rs { reg := ed.Lines.AdjustRegion(reg) if !reg.IsNil() { hlts = append(hlts, buf.RegionToView(ed.viewId, reg)) } } return hlts } hlts := rtoview(ed.Highlights) slts := rtoview(ed.scopelights) hlts = append(hlts, slts...) buf.Lock() li := 0 for ln := stln; ln <= edln; ln++ { ed.renderLine(li, ln, rpos, vsel, hlts) rpos.Y += ed.charSize.Y li++ } buf.Unlock() if ed.hasLineNumbers { pc.PopContext() } pc.PopContext() } type renderCache struct { tx []rune lns *shaped.Lines } // renderLine renders given line, dealing with tab stops etc func (ed *Base) renderLine(li, ln int, rpos math32.Vector2, vsel textpos.Region, hlts []textpos.Region) { buf := ed.Lines sh := ed.Scene.TextShaper() pc := &ed.Scene.Painter sz := ed.charSize sz.X *= float32(ed.linesSize.X) vlr := buf.ViewLineRegionLocked(ed.viewId, ln) vseli := vlr.Intersect(vsel, ed.linesSize.X) tx := buf.ViewMarkupLine(ed.viewId, ln) ctx := &rich.DefaultSettings ts := ed.Lines.Settings.TabSize indent := 0 sty, tsty := ed.Styles.NewRichText() shapeTab := func(stx rich.Text, ssz math32.Vector2) *shaped.Lines { if ed.tabRender != nil { return ed.tabRender.Clone() } lns := sh.WrapLines(stx, sty, tsty, ctx, ssz) ed.tabRender = lns return lns } shapeSpan := func(stx rich.Text, ssz math32.Vector2) *shaped.Lines { txt := stx.Join() rc := ed.lineRenders[li] if rc.lns != nil && slices.Compare(rc.tx, txt) == 0 { return rc.lns } lns := sh.WrapLines(stx, sty, tsty, ctx, ssz) ed.lineRenders[li] = renderCache{tx: txt, lns: lns} return lns } rendSpan := func(lns *shaped.Lines, pos math32.Vector2, coff int) { lns.SelectReset() lns.HighlightReset() lns.SetGlyphXAdvance(math32.ToFixed(ed.charSize.X)) if !vseli.IsNil() { lns.SelectRegion(textpos.Range{Start: vseli.Start.Char - coff, End: vseli.End.Char - coff}) } for _, hlrg := range hlts { hlsi := vlr.Intersect(hlrg, ed.linesSize.X) if !hlsi.IsNil() { lns.HighlightRegion(textpos.Range{Start: hlsi.Start.Char - coff, End: hlsi.End.Char - coff}) } } pc.DrawText(lns, pos) } for si := range tx { // tabs encoded as single chars at start sn, rn := rich.SpanLen(tx[si]) if rn == 1 && tx[si][sn] == '\t' { lpos := rpos ic := float32(ts*indent) * ed.charSize.X lpos.X += ic lsz := sz lsz.X -= ic rendSpan(shapeTab(tx[si:si+1], lsz), lpos, indent) indent++ } else { break } } rtx := tx[indent:] lpos := rpos ic := float32(ts*indent) * ed.charSize.X lpos.X += ic lsz := sz lsz.X -= ic hasTab := false for si := range rtx { sn, rn := rich.SpanLen(tx[si]) if rn > 0 && tx[si][sn] == '\t' { hasTab = true break } } if !hasTab { rendSpan(shapeSpan(rtx, lsz), lpos, indent) return } coff := indent cc := ts * indent scc := cc for si := range rtx { sn, rn := rich.SpanLen(rtx[si]) if rn == 0 { continue } spos := lpos spos.X += float32(cc-scc) * ed.charSize.X if rtx[si][sn] != '\t' { ssz := ed.charSize.Mul(math32.Vec2(float32(rn), 1)) rendSpan(shapeSpan(rtx[si:si+1], ssz), spos, coff) cc += rn coff += rn continue } for range rn { tcc := ((cc / 8) + 1) * 8 spos.X += float32(tcc-cc) * ed.charSize.X cc = tcc rendSpan(shapeTab(rtx[si:si+1], ed.charSize), spos, coff) coff++ } } } // renderLineNumbersBox renders the background for the line numbers in the LineNumberColor func (ed *Base) renderLineNumbersBox() { if !ed.hasLineNumbers { return } pc := &ed.Scene.Painter bb := ed.renderBBox() spos := math32.FromPoint(bb.Min) epos := math32.FromPoint(bb.Max) epos.X = spos.X + ed.LineNumberPixels() sz := epos.Sub(spos) pc.Fill.Color = ed.LineNumberColor pc.RoundedRectangleSides(spos.X, spos.Y, sz.X, sz.Y, ed.Styles.Border.Radius.Dots()) pc.Draw() } // renderLineNumber renders given line number at given li index. func (ed *Base) renderLineNumber(pos math32.Vector2, li, ln int) { if !ed.hasLineNumbers || ed.Lines == nil { return } pos.Y += float32(li) * ed.charSize.Y pc := &ed.Scene.Painter sh := ed.Scene.TextShaper() sty, tsty := ed.Styles.NewRichText() sty.SetBackground(nil) lfmt := fmt.Sprintf("%d", ed.lineNumberDigits) lfmt = "%" + lfmt + "d" lnstr := fmt.Sprintf(lfmt, ln+1) if ed.CursorPos.Line == ln { sty.SetFillColor(colors.ToUniform(colors.Scheme.Primary.Base)) sty.Weight = rich.Bold } else { sty.SetFillColor(colors.ToUniform(colors.Scheme.OnSurfaceVariant)) } sz := ed.charSize sz.X *= float32(ed.lineNumberOffset) var lns *shaped.Lines rc := ed.lineNoRenders[li] tx := rich.NewText(sty, []rune(lnstr)) if rc.lns != nil && slices.Compare(rc.tx, tx[0]) == 0 { // captures styling lns = rc.lns } else { lns = sh.WrapLines(tx, sty, tsty, &rich.DefaultSettings, sz) ed.lineNoRenders[li] = renderCache{tx: tx[0], lns: lns} } pc.DrawText(lns, pos) // render circle lineColor, has := ed.Lines.LineColor(ln) if has { pos.X += float32(ed.lineNumberDigits) * ed.charSize.X r := 0.7 * ed.charSize.X center := pos.AddScalar(r) center.Y += 0.3 * ed.charSize.Y center.X += 0.3 * ed.charSize.X pc.Fill.Color = lineColor pc.Circle(center.X, center.Y, r) pc.Draw() } } func (ed *Base) LineNumberPixels() float32 { return float32(ed.lineNumberOffset) * ed.charSize.X } // 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}, {4, 4, 0, 0}, {8, 8, 0, 0}, {4, 8, 0, 0}, {0, 8, 4, 0}, {0, 8, 8, 0}, {0, 4, 8, 0}, {4, 0, 8, 0}, {4, 0, 4, 0}, } // renderDepthBackground renders the depth background color. func (ed *Base) renderDepthBackground(pos math32.Vector2, stln, edln int) { if !ed.Lines.Settings.DepthColor || ed.IsDisabled() || !ed.StateIs(states.Focused) { return } pos.X += ed.LineNumberPixels() buf := ed.Lines bbmax := float32(ed.Geom.ContentBBox.Max.X) pc := &ed.Scene.Painter sty := &ed.Styles isDark := matcolor.SchemeIsDark nclrs := len(viewDepthColors) for ln := stln; ln <= edln; ln++ { sp := ed.Lines.PosFromView(ed.viewId, textpos.Pos{Line: ln}) depth := buf.LineLexDepth(sp.Line) if depth <= 0 { continue } var vdc color.RGBA if isDark { // reverse order too vdc = viewDepthColors[(nclrs-1)-(depth%nclrs)] } else { vdc = viewDepthColors[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) }) spos := pos spos.Y += float32(ln-stln) * ed.charSize.Y epos := spos epos.Y += ed.charSize.Y epos.X = bbmax pc.FillBox(spos, epos.Sub(spos), bg) } } // PixelToCursor finds the cursor position that corresponds to the given pixel // location (e.g., from mouse click), in widget-relative coordinates. func (ed *Base) PixelToCursor(pt image.Point) textpos.Pos { if ed.Lines == nil { return textpos.PosErr } stln, _, spos := ed.renderLineStartEnd() ptf := math32.FromPoint(pt) ptf.X += ed.Geom.Pos.Content.X ptf.Y -= (spos.Y - ed.Geom.Pos.Content.Y) // fractional bit cp := ptf.Div(ed.charSize) if cp.Y < 0 { return textpos.PosErr } vln := stln + int(math32.Floor(cp.Y)) vpos := textpos.Pos{Line: vln, Char: 0} srcp := ed.Lines.PosFromView(ed.viewId, vpos) stp := ed.charStartPos(srcp) if ptf.X < stp.X { return srcp } scc := srcp.Char hc := 0.5 * ed.charSize.X vll := ed.Lines.ViewLineLen(ed.viewId, vln) for cc := range vll { srcp.Char = scc + cc edp := ed.charStartPos(textpos.Pos{Line: srcp.Line, Char: scc + cc + 1}) if ptf.X >= stp.X-hc && ptf.X < edp.X-hc { return srcp } stp = edp } srcp.Char = scc + vll return srcp } // charStartPos returns the starting (top left) render coords for the // given source text position. func (ed *Base) charStartPos(pos textpos.Pos) math32.Vector2 { if ed.Lines == nil { return math32.Vector2{} } vpos := ed.Lines.PosToView(ed.viewId, pos) spos := ed.Geom.Pos.Content spos.X += ed.LineNumberPixels() - ed.Geom.Scroll.X spos.Y += (float32(vpos.Line) - ed.scrollPos) * ed.charSize.Y tx := ed.Lines.ViewMarkupLine(ed.viewId, vpos.Line) ts := ed.Lines.Settings.TabSize indent := 0 for si := range tx { // tabs encoded as single chars at start sn, rn := rich.SpanLen(tx[si]) if rn == 1 && tx[si][sn] == '\t' { if vpos.Char == si { spos.X += float32(indent*ts) * ed.charSize.X return spos } indent++ } else { break } } rtx := tx[indent:] lpos := spos lpos.X += float32(ts*indent) * ed.charSize.X coff := indent cc := ts * indent scc := cc for si := range rtx { sn, rn := rich.SpanLen(rtx[si]) if rn == 0 { continue } spos := lpos spos.X += float32(cc-scc) * ed.charSize.X if rtx[si][sn] != '\t' { rc := vpos.Char - coff if rc >= 0 && rc < rn { spos.X += float32(rc) * ed.charSize.X return spos } cc += rn coff += rn continue } for ri := range rn { if ri == vpos.Char-coff { return spos } tcc := ((cc / 8) + 1) * 8 cc = tcc coff++ } } spos = lpos spos.X += float32(cc-scc) * ed.charSize.X return spos } // 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 textcore import ( "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/base/fileinfo/mimedata" "cogentcore.org/core/base/strcase" "cogentcore.org/core/core" "cogentcore.org/core/text/lines" "cogentcore.org/core/text/textpos" ) //////// Regions // HighlightRegion adds a new highlighted region. Use HighlightsReset to // clear any existing prior to this if only one region desired. func (ed *Base) HighlightRegion(reg textpos.Region) { ed.Highlights = append(ed.Highlights, reg) ed.NeedsRender() } // HighlightsReset resets the list of all highlighted regions. func (ed *Base) HighlightsReset() { if len(ed.Highlights) == 0 { return } ed.Highlights = ed.Highlights[:0] ed.NeedsRender() } // scopelightsReset clears the scopelights slice of all regions. // does needsrender if actually reset. func (ed *Base) scopelightsReset() { if len(ed.scopelights) == 0 { return } sl := make([]textpos.Region, len(ed.scopelights)) copy(sl, ed.scopelights) ed.scopelights = ed.scopelights[:0] ed.NeedsRender() } func (ed *Base) addScopelights(st, end textpos.Pos) { ed.scopelights = append(ed.scopelights, textpos.NewRegionPos(st, textpos.Pos{st.Line, st.Char + 1})) ed.scopelights = append(ed.scopelights, textpos.NewRegionPos(end, textpos.Pos{end.Line, end.Char + 1})) } //////// Selection // clearSelected resets both the global selected flag and any current selection func (ed *Base) clearSelected() { // ed.WidgetBase.ClearSelected() ed.SelectReset() } // HasSelection returns whether there is a selected region of text func (ed *Base) HasSelection() bool { return ed.SelectRegion.Start.IsLess(ed.SelectRegion.End) } // validateSelection ensures that the selection region is still valid. func (ed *Base) validateSelection() { ed.SelectRegion.Start = ed.Lines.ValidPos(ed.SelectRegion.Start) ed.SelectRegion.End = ed.Lines.ValidPos(ed.SelectRegion.End) } // Selection returns the currently selected text as a textpos.Edit, which // captures start, end, and full lines in between -- nil if no selection func (ed *Base) Selection() *textpos.Edit { if ed.HasSelection() { ed.validateSelection() return ed.Lines.Region(ed.SelectRegion.Start, ed.SelectRegion.End) } return nil } // selectModeToggle toggles the SelectMode, updating selection with cursor movement func (ed *Base) selectModeToggle() { if ed.selectMode { ed.selectMode = false } else { ed.selectMode = true ed.selectStart = ed.CursorPos ed.selectRegionUpdate(ed.CursorPos) } ed.savePosHistory(ed.CursorPos) } // selectRegionUpdate updates current select region based on given cursor position // relative to SelectStart position func (ed *Base) selectRegionUpdate(pos textpos.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 } } // selectAll selects all the text func (ed *Base) selectAll() { ed.SelectRegion.Start = textpos.PosZero ed.SelectRegion.End = ed.Lines.EndPos() ed.NeedsRender() } // selectWord selects the word (whitespace, punctuation delimited) that the cursor is on. // returns true if word selected func (ed *Base) selectWord() bool { if ed.Lines == nil { return false } reg := ed.Lines.WordAt(ed.CursorPos) ed.SelectRegion = reg ed.selectStart = ed.SelectRegion.Start return true } // SelectReset resets the selection func (ed *Base) SelectReset() { ed.selectMode = false if !ed.HasSelection() { return } ed.SelectRegion = textpos.Region{} ed.previousSelectRegion = textpos.Region{} } //////// Undo / Redo // undo undoes previous action func (ed *Base) undo() { tbes := ed.Lines.Undo() if tbes != nil { tbe := tbes[len(tbes)-1] if tbe.Delete { // now an insert ed.SetCursorShow(tbe.Region.End) } else { ed.SetCursorShow(tbe.Region.Start) } } else { ed.SendInput() // updates status.. ed.scrollCursorToCenterIfHidden() } ed.savePosHistory(ed.CursorPos) ed.NeedsRender() } // redo redoes previously undone action func (ed *Base) redo() { tbes := ed.Lines.Redo() if tbes != nil { tbe := tbes[len(tbes)-1] if tbe.Delete { ed.SetCursorShow(tbe.Region.Start) } else { ed.SetCursorShow(tbe.Region.End) } } else { ed.scrollCursorToCenterIfHidden() } ed.savePosHistory(ed.CursorPos) ed.NeedsRender() } //////// Cut / Copy / Paste // editorClipboardHistory is the [Base] clipboard history; everything that has been copied var editorClipboardHistory [][]byte // addBaseClipboardHistory adds the given clipboard bytes to top of history stack func addBaseClipboardHistory(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 *Base) 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 *Base) Cut() *textpos.Edit { if !ed.HasSelection() { return nil } ed.validateSelection() org := ed.SelectRegion.Start cut := ed.deleteSelection() if cut != nil { cb := cut.ToBytes() ed.Clipboard().Write(mimedata.NewTextBytes(cb)) addBaseClipboardHistory(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 textpos.Edit (nil if none) func (ed *Base) deleteSelection() *textpos.Edit { ed.validateSelection() tbe := ed.Lines.DeleteText(ed.SelectRegion.Start, ed.SelectRegion.End) ed.SelectReset() return tbe } // Copy copies any selected text to the clipboard, and returns that text, // optionally resetting the current selection func (ed *Base) Copy(reset bool) *textpos.Edit { tbe := ed.Selection() if tbe == nil { return nil } cb := tbe.ToBytes() addBaseClipboardHistory(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 *Base) 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 *Base) InsertAtCursor(txt []byte) { if ed.HasSelection() { tbe := ed.deleteSelection() ed.CursorPos = tbe.AdjustPos(ed.CursorPos, textpos.AdjustPosDelStart) // move to start if in reg } cp := ed.validateCursor() tbe := ed.Lines.InsertText(cp, []rune(string(txt))) if tbe == nil { return } pos := tbe.Region.End if len(txt) == 1 && txt[0] == '\n' { pos.Char = 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 *textpos.Edit // CutRect cuts rectangle defined by selected text (upper left to lower right) // and adds it to the clipboard, also returns cut lines. func (ed *Base) CutRect() *textpos.Edit { if !ed.HasSelection() { return nil } ed.validateSelection() npos := textpos.Pos{Line: ed.SelectRegion.End.Line, Char: ed.SelectRegion.Start.Char} cut := ed.Lines.DeleteTextRect(ed.SelectRegion.Start, ed.SelectRegion.End) 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 *Base) CopyRect(reset bool) *textpos.Edit { ed.validateSelection() tbe := ed.Lines.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 *Base) PasteRect() { if editorClipboardRect == nil { return } ce := editorClipboardRect.Clone() nl := ce.Region.End.Line - ce.Region.Start.Line nch := ce.Region.End.Char - ce.Region.Start.Char ce.Region.Start.Line = ed.CursorPos.Line ce.Region.End.Line = ed.CursorPos.Line + nl ce.Region.Start.Char = ed.CursorPos.Char ce.Region.End.Char = ed.CursorPos.Char + nch tbe := ed.Lines.InsertTextRect(ce) pos := tbe.Region.End ed.SetCursorShow(pos) ed.setCursorColumn(ed.CursorPos) ed.savePosHistory(ed.CursorPos) ed.NeedsRender() } // ReCaseSelection changes the case of the currently selected lines. // Returns the new text; empty if nothing selected. func (ed *Base) ReCaseSelection(c strcase.Cases) string { if !ed.HasSelection() { return "" } sel := ed.Selection() nstr := strcase.To(string(sel.ToBytes()), c) ed.Lines.ReplaceText(sel.Region.Start, sel.Region.End, sel.Region.Start, nstr, lines.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 textcore import ( "strings" "unicode" "cogentcore.org/core/base/fileinfo" "cogentcore.org/core/events" "cogentcore.org/core/keymap" "cogentcore.org/core/text/lines" "cogentcore.org/core/text/parse/lexer" "cogentcore.org/core/text/spell" "cogentcore.org/core/text/textpos" "cogentcore.org/core/text/token" ) // 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.isSpellEnabled(ed.CursorPos) { return } isDoc := ed.Lines.FileInfo().Cat == fileinfo.Doc tp := ed.CursorPos kf := keymap.Of(kt.KeyChord()) switch kf { case keymap.MoveUp: if isDoc { ed.spellCheckLineTag(tp.Line) } case keymap.MoveDown: if isDoc { ed.spellCheckLineTag(tp.Line) } case keymap.MoveRight: if ed.Lines.IsWordEnd(tp) { reg := ed.Lines.WordBefore(tp) ed.spellCheck(reg) break } if tp.Char == 0 { // end of line tp.Line-- if tp.Line < 0 { tp.Line = 0 } if isDoc { ed.spellCheckLineTag(tp.Line) // redo prior line } tp.Char = ed.Lines.LineLen(tp.Line) reg := ed.Lines.WordBefore(tp) ed.spellCheck(reg) break } txt := ed.Lines.Line(tp.Line) var r rune atend := false if tp.Char >= len(txt) { atend = true tp.Char++ } else { r = txt[tp.Char] } if atend || textpos.IsWordBreak(r, rune(-1)) { tp.Char-- // we are one past the end of word if tp.Char < 0 { tp.Char = 0 } reg := ed.Lines.WordBefore(tp) ed.spellCheck(reg) } case keymap.Enter: tp.Line-- if tp.Line < 0 { tp.Line = 0 } if isDoc { ed.spellCheckLineTag(tp.Line) // redo prior line } tp.Char = ed.Lines.LineLen(tp.Line) reg := ed.Lines.WordBefore(tp) ed.spellCheck(reg) case keymap.FocusNext: tp.Char-- // we are one past the end of word if tp.Char < 0 { tp.Char = 0 } reg := ed.Lines.WordBefore(tp) ed.spellCheck(reg) case keymap.Backspace, keymap.Delete: if ed.Lines.IsWordMiddle(ed.CursorPos) { reg := ed.Lines.WordAt(ed.CursorPos) ed.spellCheck(ed.Lines.Region(reg.Start, reg.End)) } else { reg := ed.Lines.WordBefore(tp) ed.spellCheck(reg) } case keymap.None: if unicode.IsSpace(kt.KeyRune()) || unicode.IsPunct(kt.KeyRune()) && kt.KeyRune() != '\'' { // contractions! tp.Char-- // we are one past the end of word if tp.Char < 0 { tp.Char = 0 } reg := ed.Lines.WordBefore(tp) ed.spellCheck(reg) } else { if ed.Lines.IsWordMiddle(ed.CursorPos) { reg := ed.Lines.WordAt(ed.CursorPos) ed.spellCheck(ed.Lines.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 *textpos.Edit) bool { if ed.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.Region.Start.Char += widx reg.Region.End.Char += widx - ld sugs, knwn := ed.spell.checkWord(lwb) if knwn { ed.Lines.RemoveTag(reg.Region.Start, token.TextSpellErr) return false } // fmt.Printf("spell err: %s\n", wb) ed.spell.setWord(wb, sugs, reg.Region.Start.Line, reg.Region.Start.Char) ed.Lines.RemoveTag(reg.Region.Start, token.TextSpellErr) ed.Lines.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.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.spell.checkWord(wb) if knwn && !ed.spell.isLastLearned(wb) { return false } ed.spell.setWord(wb, sugs, tbe.Region.Start.Line, tbe.Region.Start.Char) cpos := ed.charStartPos(ed.CursorPos).ToPoint() // physical location cpos.X += 5 cpos.Y += 10 ed.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.spell == nil || ed.ISearch.On || ed.QReplace.On { return } if !ed.Lines.Settings.SpellCorrect { return } ed.spell.cancel() } // 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 (ed *Editor) isSpellEnabled(pos textpos.Pos) bool { if ed.spell == nil || !ed.Lines.Settings.SpellCorrect { return false } switch ed.Lines.FileInfo().Cat { case fileinfo.Doc: // not in code! return !ed.Lines.InTokenCode(pos) case fileinfo.Code: return ed.Lines.InComment(pos) || ed.Lines.InLitString(pos) default: return false } } // setSpell sets spell correct functions so that spell correct will // automatically be offered as the user types func (ed *Editor) setSpell() { if ed.spell != nil { return } initSpell() ed.spell = newSpell() ed.spell.onSelect(func(e events.Event) { ed.correctText(ed.spell.correction) }) } // correctText edits the text using the string chosen from the correction menu func (ed *Editor) correctText(s string) { st := textpos.Pos{ed.spell.srcLn, ed.spell.srcCh} // start of word ed.Lines.RemoveTag(st, token.TextSpellErr) oend := st oend.Char += len(ed.spell.word) ed.Lines.ReplaceText(st, oend, st, s, lines.ReplaceNoMatchCase) ep := st ep.Char += len(s) ed.SetCursorShow(ep) } // SpellCheckLineErrors runs spell check on given line, and returns Lex tags // with token.TextSpellErr for any misspelled words func (ed *Editor) SpellCheckLineErrors(ln int) lexer.Line { if !ed.Lines.IsValidLine(ln) { return nil } return spell.CheckLexLine(ed.Lines.Line(ln), ed.Lines.HiTags(ln)) } // spellCheckLineTag runs spell check on given line, and sets Tags for any // misspelled words and updates markup for that line. func (ed *Editor) spellCheckLineTag(ln int) { if !ed.Lines.IsValidLine(ln) { return } ser := ed.SpellCheckLineErrors(ln) ntgs := ed.Lines.AdjustedTags(ln) ntgs.DeleteToken(token.TextSpellErr) for _, t := range ser { ntgs.AddSort(t) } ed.Lines.SetTags(ln, ntgs) ed.Lines.MarkupLines(ln, ln) ed.Lines.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 textcore // 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/text/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) 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 textcore import ( "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/styles" "cogentcore.org/core/text/lines" "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 *lines.Lines `json:"-" xml:"-"` // [Buffer] for B BufferB *lines.Lines `json:"-" xml:"-"` inInputEvent bool } func (te *TwinEditors) Init() { te.Splits.Init() te.BufferA = lines.NewLines() te.BufferB = lines.NewLines() f := func(name string, buf *lines.Lines) { tree.AddChildAt(te, name, func(w *Editor) { w.SetLines(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.SetFilename(fileA) te.BufferA.Stat() // update markup te.BufferB.SetFilename(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.updateScroll(me.scrollPos) 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 textcore import ( "image" "io" "time" "cogentcore.org/core/core" "cogentcore.org/core/styles/units" "cogentcore.org/core/text/lines" "cogentcore.org/core/text/rich" "cogentcore.org/core/tree" "cogentcore.org/core/types" ) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/textcore.Base", IDName: "base", Doc: "Base is a widget with basic infrastructure for viewing and editing\n[lines.Lines] of monospaced text, used in [textcore.Editor] and\nterminal. There can be multiple Base widgets for each lines buffer.\n\nUse NeedsRender to drive an render update for any change that does\nnot change the line-level layout of the text.\n\nAll updating in the Base should be within a single goroutine,\nas it would require extensive protections throughout code otherwise.", Directives: []types.Directive{{Tool: "core", Directive: "embedder"}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Lines", Doc: "Lines is the text lines content for this editor."}, {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: "AutoscrollOnInput", Doc: "AutoscrollOnInput scrolls the display to the end when Input events are received."}, {Name: "viewId", Doc: "viewId is the unique id of the Lines view."}, {Name: "charSize", Doc: "charSize is the render size of one character (rune).\nY = line height, X = total glyph advance."}, {Name: "visSizeAlloc", Doc: "visSizeAlloc is the Geom.Size.Alloc.Total subtracting extra space,\navailable for rendering text lines and line numbers."}, {Name: "lastVisSizeAlloc", Doc: "lastVisSizeAlloc is the last visSizeAlloc used in laying out lines.\nIt is used to trigger a new layout only when needed."}, {Name: "visSize", Doc: "visSize is the height in lines and width in chars of the visible area."}, {Name: "linesSize", Doc: "linesSize is the height in lines and width in chars of the Lines text area,\n(excluding line numbers), which can be larger than the visSize."}, {Name: "scrollPos", Doc: "scrollPos is the position of the scrollbar, in units of lines of text.\nfractional scrolling is supported."}, {Name: "hasLineNumbers", Doc: "hasLineNumbers indicates that this editor has line numbers\n(per [Editor] option)"}, {Name: "lineNumberOffset", Doc: "lineNumberOffset is the horizontal offset in chars for the start of text\nafter line numbers. This is 0 if no line numbers."}, {Name: "totalSize", Doc: "totalSize is total size of all text, including line numbers,\nmultiplied by charSize."}, {Name: "lineNumberDigits", Doc: "lineNumberDigits is the number of line number digits needed."}, {Name: "CursorPos", Doc: "CursorPos is the current cursor position."}, {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: "isScrolling", Doc: "isScrolling is true when scrolling: prevents keeping current cursor position\nin view."}, {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\nlast 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\nbe the start or end of selected region depending 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\nregions, e.g., for search results."}, {Name: "scopelights", Doc: "scopelights is a slice of regions representing the highlighted\nregions 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: "lineRenders", Doc: "lineRenders are the cached rendered lines of text."}, {Name: "lineNoRenders", Doc: "lineNoRenders are the cached rendered line numbers"}, {Name: "tabRender", Doc: "tabRender is a shaped tab"}, {Name: "selectMode", Doc: "selectMode is a boolean indicating whether to select text as the cursor moves."}, {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"}}}) // NewBase returns a new [Base] with the given optional parent: // Base is a widget with basic infrastructure for viewing and editing // [lines.Lines] of monospaced text, used in [textcore.Editor] and // terminal. There can be multiple Base widgets for each lines buffer. // // Use NeedsRender to drive an render update for any change that does // not change the line-level layout of the text. // // All updating in the Base should be within a single goroutine, // as it would require extensive protections throughout code otherwise. func NewBase(parent ...tree.Node) *Base { return tree.New[Base](parent...) } // BaseEmbedder is an interface that all types that embed Base satisfy type BaseEmbedder interface { AsBase() *Base } // AsBase returns the given value as a value of type Base if the type // of the given value embeds Base, or nil otherwise func AsBase(n tree.Node) *Base { if t, ok := n.(BaseEmbedder); ok { return t.AsBase() } return nil } // AsBase satisfies the [BaseEmbedder] interface func (t *Base) AsBase() *Base { return t } // SetCursorWidth sets the [Base.CursorWidth]: // CursorWidth is the width of the cursor. // This should be set in Stylers like all other style properties. func (t *Base) SetCursorWidth(v units.Value) *Base { t.CursorWidth = v; return t } // SetLineNumberColor sets the [Base.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 *Base) SetLineNumberColor(v image.Image) *Base { t.LineNumberColor = v; return t } // SetSelectColor sets the [Base.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 *Base) SetSelectColor(v image.Image) *Base { t.SelectColor = v; return t } // SetHighlightColor sets the [Base.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 *Base) SetHighlightColor(v image.Image) *Base { t.HighlightColor = v; return t } // SetCursorColor sets the [Base.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 *Base) SetCursorColor(v image.Image) *Base { t.CursorColor = v; return t } // SetAutoscrollOnInput sets the [Base.AutoscrollOnInput]: // AutoscrollOnInput scrolls the display to the end when Input events are received. func (t *Base) SetAutoscrollOnInput(v bool) *Base { t.AutoscrollOnInput = v; return t } // SetLinkHandler sets the [Base.LinkHandler]: // LinkHandler handles link clicks. // If it is nil, they are sent to the standard web URL handler. func (t *Base) SetLinkHandler(v func(tl *rich.Hyperlink)) *Base { t.LinkHandler = v; return t } var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/textcore.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: "linesA", Doc: "[lines.Lines] for A showing the aligned edit view"}, {Name: "linesB", Doc: "[lines.Lines] for B showing the aligned edit view"}, {Name: "alignD", Doc: "aligned diffs records diff for aligned lines"}, {Name: "diffs", Doc: "diffs applied"}, {Name: "inInputEvent"}, {Name: "toolbar"}}}) // 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/text/textcore.DiffTextEditor", IDName: "diff-text-editor", Doc: "DiffTextEditor supports double-click based application of edits from one\nlines 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 // lines to the other. func NewDiffTextEditor(parent ...tree.Node) *DiffTextEditor { return tree.New[DiffTextEditor](parent...) } var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/textcore.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\n[lines.Lines] buffer 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.\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"}}}, {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.\nDo NOT use this in an OnChange event handler as it emits a Change event! Use\n[Editor.SaveQuiet] instead.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}, {Name: "SaveQuiet", Doc: "SaveQuiet saves the current text into the current filename associated with this buffer.\nThis version does not emit a change event, so it is safe to use\nin an OnChange event handler, unlike [Editor.Save].", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Base"}}, Fields: []types.Field{{Name: "ISearch", Doc: "ISearch is the interactive search data."}, {Name: "QReplace", Doc: "QReplace is the query replace data."}, {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: "curFilename", Doc: "curFilename is the current filename from Lines. Used to detect changed file."}}}) // 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 // [lines.Lines] 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. // // 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 } // SetComplete sets the [Editor.Complete]: // Complete is the functions and data for text completion. func (t *Editor) SetComplete(v *core.Complete) *Editor { t.Complete = v; return t } var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/textcore.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 text.", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Embeds: []types.Field{{Name: "Mutex", Doc: "mutex protecting updates"}}, Fields: []types.Field{{Name: "Output", Doc: "the output that we are reading from, as an io.Reader"}, {Name: "Lines", Doc: "the [lines.Lines] that we output to"}, {Name: "Batch", Doc: "how much time to wait while batching output (default: 200ms)"}, {Name: "MarkupFunc", Doc: "MarkupFunc is an optional markup function that adds html tags to given line\nof output. It is essential that it not add any new text, just splits into spans\nwith different styles."}, {Name: "bufferedLines", Doc: "current buffered output raw lines, which are not yet sent to the Buffer"}, {Name: "bufferedMarkup", Doc: "current buffered output markup lines, which are not yet sent to the Buffer"}, {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\nimmediately 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 } // SetLines sets the [OutputBuffer.Lines]: // the [lines.Lines] that we output to func (t *OutputBuffer) SetLines(v *lines.Lines) *OutputBuffer { t.Lines = 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]: // MarkupFunc is an optional markup function that adds html tags to given line // of output. It is essential that it not add any new text, just splits into spans // with different styles. func (t *OutputBuffer) SetMarkupFunc(v OutputBufferMarkupFunc) *OutputBuffer { t.MarkupFunc = v return t } var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/textcore.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 *lines.Lines) *TwinEditors { t.BufferA = v; return t } // SetBufferB sets the [TwinEditors.BufferB]: // [Buffer] for B func (t *TwinEditors) SetBufferB(v *lines.Lines) *TwinEditors { t.BufferB = v; return t } // 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 textpos //go:generate core generate import ( "fmt" "slices" "time" "cogentcore.org/core/text/runes" ) // Edit describes an edit action to line-based text, operating on // a [Region] of the text. // Actions are only deletions and insertions (a change is a sequence // of each, given normal editing processes). type Edit struct { // Region for the edit, specifying the region to delete, or the size // of the region to insert, corresponding to the Text. // Also contains the Time stamp for this edit. Region Region // Text deleted or inserted, in rune lines. For Rect this is the // spanning character distance per line, times number of lines. Text [][]rune // Group is the optional grouping number, for grouping edits in Undo for example. Group int // Delete indicates a deletion, otherwise an insertion. Delete bool // Rect is a rectangular region with upper left corner = Region.Start // and lower right corner = Region.End. // Otherwise it is for the full continuous region. Rect bool } // NewEditFromRunes returns a 0-based edit from given runes. func NewEditFromRunes(text []rune) *Edit { if len(text) == 0 { return &Edit{} } lns := runes.Split(text, []rune("\n")) nl := len(lns) ec := len(lns[nl-1]) ed := &Edit{} ed.Region = NewRegion(0, 0, nl-1, ec) ed.Text = lns return ed } // 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 } // 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 Pos, del AdjustPosDel) Pos { if te == nil { return pos } if pos.IsLess(te.Region.Start) || pos == te.Region.Start { return pos } dl := te.Region.End.Line - te.Region.Start.Line if pos.Line > te.Region.End.Line { if te.Delete { pos.Line -= dl } else { pos.Line += dl } return pos } if te.Delete { if pos.Line < te.Region.End.Line || pos.Char < te.Region.End.Char { switch del { case AdjustPosDelStart: return te.Region.Start case AdjustPosDelEnd: return te.Region.End case AdjustPosDelErr: return PosErr } } // this means pos.Line == te.Region.End.Line, Ch >= end if dl == 0 { pos.Char -= (te.Region.End.Char - te.Region.Start.Char) } else { pos.Char -= te.Region.End.Char } } else { if dl == 0 { pos.Char += (te.Region.End.Char - te.Region.Start.Char) } else { pos.Line += dl } } return pos } // AdjustPosDel determines what to do with positions within deleted region type AdjustPosDel int32 //enums:enum // 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.Copy(te) return rc } // Copy copies from other Edit, making a clone of the source text. func (te *Edit) Copy(cp *Edit) { *te = *cp nl := len(cp.Text) if nl == 0 { te.Text = nil return } te.Text = make([][]rune, nl) for i, r := range cp.Text { te.Text[i] = slices.Clone(r) } } // 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 Pos, t time.Time, del AdjustPosDel) Pos { if te == nil { return pos } if te.Region.IsAfterTime(t) { return te.AdjustPos(pos, del) } return pos } // AdjustRegion 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. func (te *Edit) AdjustRegion(reg Region) Region { if te == nil { return reg } if !reg.Time.IsZero() && !te.Region.IsAfterTime(reg.Time.Time()) { return reg } reg.Start = te.AdjustPos(reg.Start, AdjustPosDelEnd) reg.End = te.AdjustPos(reg.End, AdjustPosDelStart) if reg.IsNil() { return Region{} } return reg } func (te *Edit) String() string { str := te.Region.String() if te.Rect { str += " [Rect]" } if te.Delete { str += " [Delete]" } str += fmt.Sprintf(" Gp: %d\n", te.Group) for li := range te.Text { str += fmt.Sprintf("%d\t%s\n", li, string(te.Text[li])) } return str } // Code generated by "core generate"; DO NOT EDIT. package textpos import ( "cogentcore.org/core/enums" ) var _AdjustPosDelValues = []AdjustPosDel{0, 1, 2} // AdjustPosDelN is the highest valid value for type AdjustPosDel, plus one. const AdjustPosDelN AdjustPosDel = 3 var _AdjustPosDelValueMap = map[string]AdjustPosDel{`AdjustPosDelErr`: 0, `AdjustPosDelStart`: 1, `AdjustPosDelEnd`: 2} var _AdjustPosDelDescMap = map[AdjustPosDel]string{0: `AdjustPosDelErr means return a PosErr when in deleted region.`, 1: `AdjustPosDelStart means return start of deleted region.`, 2: `AdjustPosDelEnd means return end of deleted region.`} var _AdjustPosDelMap = map[AdjustPosDel]string{0: `AdjustPosDelErr`, 1: `AdjustPosDelStart`, 2: `AdjustPosDelEnd`} // String returns the string representation of this AdjustPosDel value. func (i AdjustPosDel) String() string { return enums.String(i, _AdjustPosDelMap) } // SetString sets the AdjustPosDel value from its string representation, // and returns an error if the string is invalid. func (i *AdjustPosDel) SetString(s string) error { return enums.SetString(i, s, _AdjustPosDelValueMap, "AdjustPosDel") } // Int64 returns the AdjustPosDel value as an int64. func (i AdjustPosDel) Int64() int64 { return int64(i) } // SetInt64 sets the AdjustPosDel value from an int64. func (i *AdjustPosDel) SetInt64(in int64) { *i = AdjustPosDel(in) } // Desc returns the description of the AdjustPosDel value. func (i AdjustPosDel) Desc() string { return enums.Desc(i, _AdjustPosDelDescMap) } // AdjustPosDelValues returns all possible values for the type AdjustPosDel. func AdjustPosDelValues() []AdjustPosDel { return _AdjustPosDelValues } // Values returns all possible values for the type AdjustPosDel. func (i AdjustPosDel) Values() []enums.Enum { return enums.Values(_AdjustPosDelValues) } // MarshalText implements the [encoding.TextMarshaler] interface. func (i AdjustPosDel) MarshalText() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *AdjustPosDel) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "AdjustPosDel") } // 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 textpos // Match records one match for search within file, positions in runes. type Match struct { // Region of the match. Column positions are in runes. Region Region // Text surrounding the match, at most MatchContext on either side // (within a single line). Text []rune // TextMatch has the Range within the Text where the match is. TextMatch Range } func (m *Match) String() string { return m.Region.String() + ": " + string(m.Text) } // MatchContext is how much text to include on either side of the match. var MatchContext = 30 // 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-MatchContext, 0) cied := min(ed+MatchContext, sz) sctx := rn[cist:st] fstr := rn[st:ed] ectx := rn[ed:cied] tlen := len(sctx) + len(fstr) + len(ectx) txt := make([]rune, tlen) copy(txt, sctx) ti := st - cist copy(txt[ti:], fstr) ti += len(fstr) copy(txt[ti:], ectx) return Match{Region: reg, Text: txt, TextMatch: Range{Start: len(sctx), End: len(sctx) + len(fstr)}} } 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 ) // 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 textpos import ( "fmt" "strings" ) // Pos is a text position in terms of line and character index within a line, // using 0-based line numbers, which are converted to 1 base for the String() // representation. Char positions are always in runes, and can also // be used for other units such as tokens, spans, or runs. type Pos struct { Line int Char int } // AddLine returns a Pos with Line number added. func (ps Pos) AddLine(ln int) Pos { ps.Line += ln return ps } // AddChar returns a Pos with Char number added. func (ps Pos) AddChar(ch int) Pos { ps.Char += ch return ps } // String satisfies the fmt.Stringer interferace func (ps Pos) String() string { s := fmt.Sprintf("%d", ps.Line+1) if ps.Char != 0 { s += fmt.Sprintf(":%d", ps.Char) } return s } var ( // PosErr represents an error text position (-1 for both line and char) // used as a return value for cases where error positions are possible. PosErr = Pos{-1, -1} PosZero = Pos{} ) // IsLess returns true if receiver position is less than given comparison. func (ps Pos) IsLess(cmp Pos) bool { switch { case ps.Line < cmp.Line: return true case ps.Line == cmp.Line: return ps.Char < cmp.Char 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.Line, &ps.Char) ps.Line-- // link is 1-based, we use 0-based ps.Char-- // ditto case lidx >= 0: fmt.Sscanf(link, "L%d", &ps.Line) ps.Line-- // link is 1-based, we use 0-based case cidx >= 0: fmt.Sscanf(link, "C%d", &ps.Char) ps.Char-- default: // todo: could support other formats return false } 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 textpos // Range defines a range with a start and end index, where end is typically // exclusive, as in standard slice indexing and for loop conventions. type Range struct { // St is the starting index of the range. Start int // Ed is the ending index of the range. End int } // Len returns the length of the range: End - Start. func (r Range) Len() int { return r.End - r.Start } // Contains returns true if range contains given index. func (r Range) Contains(i int) bool { return i >= r.Start && i < r.End } // Intersect returns the intersection of two ranges. // If they do not overlap, then the Start and End will be -1 func (r Range) Intersect(o Range) Range { o.Start = max(o.Start, r.Start) o.End = min(o.End, r.End) if o.Len() <= 0 { return Range{-1, -1} } return o } // 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 textpos import ( "fmt" "strings" "time" "cogentcore.org/core/base/nptime" ) var RegionZero = Region{} // Region is a contiguous region within a source file with lines of rune chars, // defined by start and end [Pos] positions. // End.Char position is _exclusive_ so the last char is the one before End.Char. // End.Line position is _inclusive_, so the last line is End.Line. // There is a Time stamp for when the region was created as valid positions // into the lines source, which is critical for tracking edits in live documents. type Region struct { // Start is the starting position of region. Start Pos // End is the ending position of region. // Char position is _exclusive_ so the last char is the one before End.Char. // Line position is _inclusive_, so the last line is End.Line. End 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 } // NewRegion creates a new text region using separate line and char // values for start and end. Sets timestamp to now. func NewRegion(stLn, stCh, edLn, edCh int) Region { tr := Region{Start: Pos{Line: stLn, Char: stCh}, End: Pos{Line: edLn, Char: edCh}} tr.TimeNow() return tr } // NewRegionPos creates a new text region using position values. // Sets timestamp to now. func NewRegionPos(st, ed Pos) Region { tr := Region{Start: st, End: ed} tr.TimeNow() return tr } // NewRegionLen makes a new Region from a starting point and a length // along same line. Sets timestamp to now. func NewRegionLen(start Pos, len int) Region { tr := Region{Start: start} tr.End = start tr.End.Char += len tr.TimeNow() return tr } // 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) } // Contains returns true if region contains given position. func (tr Region) Contains(ps Pos) bool { return ps.IsLess(tr.End) && (tr.Start == ps || tr.Start.IsLess(ps)) } // ContainsLine returns true if line is within region func (tr Region) ContainsLine(ln int) bool { return tr.Start.Line >= ln && ln <= tr.End.Line } // NumLines is the number of lines in this region, based on inclusive end line. func (tr Region) NumLines() int { return 1 + (tr.End.Line - tr.Start.Line) } // Intersect returns the intersection of this region with given // other region, where the other region is assumed to be the larger, // constraining region, within which you are fitting the receiver region. // Char level start / end are only constrained if on same Start / End line. // The given endChar value is used for the end of an interior line. func (tr Region) Intersect(or Region, endChar int) Region { switch { case tr.Start.Line < or.Start.Line: tr.Start = or.Start case tr.Start.Line == or.Start.Line: tr.Start.Char = max(tr.Start.Char, or.Start.Char) case tr.Start.Line < or.End.Line: tr.Start.Char = 0 case tr.Start.Line == or.End.Line: tr.Start.Char = min(tr.Start.Char, or.End.Char-1) default: return Region{} // not in bounds } if tr.End.Line == tr.Start.Line { // keep valid tr.End.Char = max(tr.End.Char, tr.Start.Char) } switch { case tr.End.Line < or.End.Line: tr.End.Char = endChar case tr.End.Line == or.End.Line: tr.End.Char = min(tr.End.Char, or.End.Char) } return tr } // ShiftLines returns a new Region with the start and End lines // shifted by given number of lines. func (tr Region) ShiftLines(ln int) Region { tr.Start.Line += ln tr.End.Line += ln return tr } // MoveToLine returns a new Region with the Start line // set to given line. func (tr Region) MoveToLine(ln int) Region { nl := tr.NumLines() tr.Start.Line = 0 tr.End.Line = nl - 1 return tr } //////// Time // TimeNow grabs the current time as the edit time. func (tr *Region) TimeNow() { tr.Time.Now() } // 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()) } // FromStringURL decodes text region from a string representation of form: // [#]LxxCxx-LxxCxx. Used in e.g., URL links. returns true if successful func (tr *Region) FromStringURL(link string) bool { link = strings.TrimPrefix(link, "#") fmt.Sscanf(link, "L%dC%d-L%dC%d", &tr.Start.Line, &tr.Start.Char, &tr.End.Line, &tr.End.Char) return true } func (tr *Region) String() string { return fmt.Sprintf("[%s - %s]", tr.Start, tr.End) } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package textpos import "unicode" // RuneIsWordBreak returns true if given rune counts as a word break // for the purposes of selecting words. func RuneIsWordBreak(r rune) bool { return unicode.IsSpace(r) || unicode.IsSymbol(r) || unicode.IsPunct(r) } // 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 { return RuneIsWordBreak(r1) } if unicode.IsSpace(r1) || unicode.IsSymbol(r1) { return true } if unicode.IsPunct(r1) && r1 != rune('\'') { return true } if unicode.IsPunct(r1) && r1 == rune('\'') { return unicode.IsSpace(r2) || unicode.IsSymbol(r2) || unicode.IsPunct(r2) } return false } // WordAt returns the range for a word within given text starting at given // position index. If the current position is a word break then go to next // break after the first non-break. func WordAt(txt []rune, pos int) Range { var rg Range sz := len(txt) if sz == 0 { return rg } if pos < 0 { pos = 0 } if pos >= sz { pos = sz - 1 } rg.Start = pos if !RuneIsWordBreak(txt[rg.Start]) { for rg.Start > 0 { if RuneIsWordBreak(txt[rg.Start-1]) { break } rg.Start-- } rg.End = pos + 1 for rg.End < sz { if RuneIsWordBreak(txt[rg.End]) { break } rg.End++ } return rg } // keep the space start -- go to next space.. rg.End = pos + 1 for rg.End < sz { if !RuneIsWordBreak(txt[rg.End]) { break } rg.End++ } for rg.End < sz { if RuneIsWordBreak(txt[rg.End]) { break } rg.End++ } return rg } // ForwardWord moves position index forward by words, for given // number of steps. Returns the number of steps actually moved, // given the amount of text available. func ForwardWord(txt []rune, pos, steps int) (wpos, nstep int) { sz := len(txt) if sz == 0 { return 0, 0 } if pos >= sz-1 { return sz - 1, 0 } if pos < 0 { pos = 0 } for range steps { if pos == sz-1 { break } ch := pos for ch < sz-1 { // if on a wb, go past if !IsWordBreak(txt[ch], txt[ch+1]) { break } ch++ } for ch < sz-1 { // now go to next wb if IsWordBreak(txt[ch], txt[ch+1]) { break } ch++ } pos = ch nstep++ } return pos, nstep } // BackwardWord moves position index backward by words, for given // number of steps. Returns the number of steps actually moved, // given the amount of text available. func BackwardWord(txt []rune, pos, steps int) (wpos, nstep int) { sz := len(txt) if sz == 0 { return 0, 0 } if pos <= 0 { return 0, 0 } if pos >= sz { pos = sz - 1 } for range steps { if pos == 0 { break } ch := pos for ch > 0 { // if on a wb, go past if !IsWordBreak(txt[ch], txt[ch-1]) { break } ch-- } for ch > 0 { // now go to next wb if IsWordBreak(txt[ch], txt[ch-1]) { break } ch-- } pos = ch nstep++ } return pos, nstep } // 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 } 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) 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) } // IsNil returns true if the Node interface is nil, or the underlying // This pointer is nil, which happens when the node is deleted. func IsNil(n Node) bool { return n == nil || n.AsTree().This == nil } // 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()] `table:"-" 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)] `table:"-" 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) `table:"-" 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) { tot := to.AsTree() fromt := from.AsTree() fc := fromt.Children if len(fc) == 0 { tot.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(&tot.Children, to, p) } if fromt.Properties != nil { if tot.Properties == nil { tot.Properties = map[string]any{} } maps.Copy(tot.Properties, fromt.Properties) } tot.This.CopyFieldsFrom(from) for i, kid := range tot.Children { fmk := fromt.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 typgen provides the generation of type information for // Go types, methods, and functions. package typegen //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(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 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, "enumgen.go", "typegen.go") 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 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/text/lines" ) // 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 lines.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 := lines.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 'yaegi extract cogentcore.org/core/base/errors'. DO NOT EDIT. package basesymbols 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 basesymbols 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), "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), "Goal": reflect.ValueOf(fileinfo.Goal), "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), "IsGeneratedFile": reflect.ValueOf(fileinfo.IsGeneratedFile), "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), "SQL": reflect.ValueOf(fileinfo.SQL), "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 basesymbols 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 "CopyFile": reflect.ValueOf(fsx.CopyFile), "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), // type definitions "Filename": reflect.ValueOf((*fsx.Filename)(nil)), } } // Code generated by 'yaegi extract cogentcore.org/core/base/labels'. DO NOT EDIT. package basesymbols 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/num'. DO NOT EDIT. package basesymbols import ( "reflect" ) func init() { Symbols["cogentcore.org/core/base/num/num"] = map[string]reflect.Value{} } // Code generated by 'yaegi extract cogentcore.org/core/base/reflectx'. DO NOT EDIT. package basesymbols 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), "CopyFields": reflect.ValueOf(reflectx.CopyFields), "CopyMapRobust": reflect.ValueOf(reflectx.CopyMapRobust), "CopySliceRobust": reflect.ValueOf(reflectx.CopySliceRobust), "FieldByPath": reflect.ValueOf(reflectx.FieldByPath), "FormatDefault": reflect.ValueOf(reflectx.FormatDefault), "IsNil": reflect.ValueOf(reflectx.IsNil), "KindIsBasic": reflect.ValueOf(reflectx.KindIsBasic), "KindIsFloat": reflect.ValueOf(reflectx.KindIsFloat), "KindIsInt": reflect.ValueOf(reflectx.KindIsInt), "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), "SetFieldsFromMap": reflect.ValueOf(reflectx.SetFieldsFromMap), "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/base/strcase'. DO NOT EDIT. package basesymbols 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/math32'. DO NOT EDIT. package basesymbols 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), "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), "Rotate2DAround": reflect.ValueOf(math32.Rotate2DAround), "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), "Vector2Polar": reflect.ValueOf(math32.Vector2Polar), "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 fmt'. DO NOT EDIT. //go:build go1.22 // +build go1.22 package basesymbols 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 log/slog'. DO NOT EDIT. //go:build go1.22 // +build go1.22 package basesymbols 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 math'. DO NOT EDIT. //go:build go1.22 // +build go1.22 package basesymbols import ( "go/constant" "go/token" "math" "reflect" ) func init() { Symbols["math/math"] = map[string]reflect.Value{ // function, constant and variable definitions "Abs": reflect.ValueOf(math.Abs), "Acos": reflect.ValueOf(math.Acos), "Acosh": reflect.ValueOf(math.Acosh), "Asin": reflect.ValueOf(math.Asin), "Asinh": reflect.ValueOf(math.Asinh), "Atan": reflect.ValueOf(math.Atan), "Atan2": reflect.ValueOf(math.Atan2), "Atanh": reflect.ValueOf(math.Atanh), "Cbrt": reflect.ValueOf(math.Cbrt), "Ceil": reflect.ValueOf(math.Ceil), "Copysign": reflect.ValueOf(math.Copysign), "Cos": reflect.ValueOf(math.Cos), "Cosh": reflect.ValueOf(math.Cosh), "Dim": reflect.ValueOf(math.Dim), "E": reflect.ValueOf(constant.MakeFromLiteral("2.71828182845904523536028747135266249775724709369995957496696762566337824315673231520670375558666729784504486779277967997696994772644702281675346915668215131895555530285035761295375777990557253360748291015625", token.FLOAT, 0)), "Erf": reflect.ValueOf(math.Erf), "Erfc": reflect.ValueOf(math.Erfc), "Erfcinv": reflect.ValueOf(math.Erfcinv), "Erfinv": reflect.ValueOf(math.Erfinv), "Exp": reflect.ValueOf(math.Exp), "Exp2": reflect.ValueOf(math.Exp2), "Expm1": reflect.ValueOf(math.Expm1), "FMA": reflect.ValueOf(math.FMA), "Float32bits": reflect.ValueOf(math.Float32bits), "Float32frombits": reflect.ValueOf(math.Float32frombits), "Float64bits": reflect.ValueOf(math.Float64bits), "Float64frombits": reflect.ValueOf(math.Float64frombits), "Floor": reflect.ValueOf(math.Floor), "Frexp": reflect.ValueOf(math.Frexp), "Gamma": reflect.ValueOf(math.Gamma), "Hypot": reflect.ValueOf(math.Hypot), "Ilogb": reflect.ValueOf(math.Ilogb), "Inf": reflect.ValueOf(math.Inf), "IsInf": reflect.ValueOf(math.IsInf), "IsNaN": reflect.ValueOf(math.IsNaN), "J0": reflect.ValueOf(math.J0), "J1": reflect.ValueOf(math.J1), "Jn": reflect.ValueOf(math.Jn), "Ldexp": reflect.ValueOf(math.Ldexp), "Lgamma": reflect.ValueOf(math.Lgamma), "Ln10": reflect.ValueOf(constant.MakeFromLiteral("2.30258509299404568401799145468436420760110148862877297603332784146804725494827975466552490443295866962642372461496758838959542646932914211937012833592062802600362869664962772731087170541286468505859375", token.FLOAT, 0)), "Ln2": reflect.ValueOf(constant.MakeFromLiteral("0.6931471805599453094172321214581765680755001343602552541206800092715999496201383079363438206637927920954189307729314303884387720696314608777673678644642390655170150035209453154294578780536539852619171142578125", token.FLOAT, 0)), "Log": reflect.ValueOf(math.Log), "Log10": reflect.ValueOf(math.Log10), "Log10E": reflect.ValueOf(constant.MakeFromLiteral("0.43429448190325182765112891891660508229439700580366656611445378416636798190620320263064286300825210972160277489744884502676719847561509639618196799746596688688378591625127711495224502868950366973876953125", token.FLOAT, 0)), "Log1p": reflect.ValueOf(math.Log1p), "Log2": reflect.ValueOf(math.Log2), "Log2E": reflect.ValueOf(constant.MakeFromLiteral("1.44269504088896340735992468100189213742664595415298593413544940772066427768997545329060870636212628972710992130324953463427359402479619301286929040235571747101382214539290471666532766903401352465152740478515625", token.FLOAT, 0)), "Logb": reflect.ValueOf(math.Logb), "Max": reflect.ValueOf(math.Max), "MaxFloat32": reflect.ValueOf(constant.MakeFromLiteral("340282346638528859811704183484516925440", token.FLOAT, 0)), "MaxFloat64": reflect.ValueOf(constant.MakeFromLiteral("179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368", token.FLOAT, 0)), "MaxInt": reflect.ValueOf(constant.MakeFromLiteral("9223372036854775807", token.INT, 0)), "MaxInt16": reflect.ValueOf(constant.MakeFromLiteral("32767", token.INT, 0)), "MaxInt32": reflect.ValueOf(constant.MakeFromLiteral("2147483647", token.INT, 0)), "MaxInt64": reflect.ValueOf(constant.MakeFromLiteral("9223372036854775807", token.INT, 0)), "MaxInt8": reflect.ValueOf(constant.MakeFromLiteral("127", token.INT, 0)), "MaxUint": reflect.ValueOf(constant.MakeFromLiteral("18446744073709551615", token.INT, 0)), "MaxUint16": reflect.ValueOf(constant.MakeFromLiteral("65535", token.INT, 0)), "MaxUint32": reflect.ValueOf(constant.MakeFromLiteral("4294967295", token.INT, 0)), "MaxUint64": reflect.ValueOf(constant.MakeFromLiteral("18446744073709551615", token.INT, 0)), "MaxUint8": reflect.ValueOf(constant.MakeFromLiteral("255", token.INT, 0)), "Min": reflect.ValueOf(math.Min), "MinInt": reflect.ValueOf(constant.MakeFromLiteral("-9223372036854775808", token.INT, 0)), "MinInt16": reflect.ValueOf(constant.MakeFromLiteral("-32768", token.INT, 0)), "MinInt32": reflect.ValueOf(constant.MakeFromLiteral("-2147483648", token.INT, 0)), "MinInt64": reflect.ValueOf(constant.MakeFromLiteral("-9223372036854775808", token.INT, 0)), "MinInt8": reflect.ValueOf(constant.MakeFromLiteral("-128", token.INT, 0)), "Mod": reflect.ValueOf(math.Mod), "Modf": reflect.ValueOf(math.Modf), "NaN": reflect.ValueOf(math.NaN), "Nextafter": reflect.ValueOf(math.Nextafter), "Nextafter32": reflect.ValueOf(math.Nextafter32), "Phi": reflect.ValueOf(constant.MakeFromLiteral("1.6180339887498948482045868343656381177203091798057628621354486119746080982153796619881086049305501566952211682590824739205931370737029882996587050475921915678674035433959321750307935872115194797515869140625", token.FLOAT, 0)), "Pi": reflect.ValueOf(constant.MakeFromLiteral("3.141592653589793238462643383279502884197169399375105820974944594789982923695635954704435713335896673485663389728754819466702315787113662862838515639906529162340867271374644786874341662041842937469482421875", token.FLOAT, 0)), "Pow": reflect.ValueOf(math.Pow), "Pow10": reflect.ValueOf(math.Pow10), "Remainder": reflect.ValueOf(math.Remainder), "Round": reflect.ValueOf(math.Round), "RoundToEven": reflect.ValueOf(math.RoundToEven), "Signbit": reflect.ValueOf(math.Signbit), "Sin": reflect.ValueOf(math.Sin), "Sincos": reflect.ValueOf(math.Sincos), "Sinh": reflect.ValueOf(math.Sinh), "SmallestNonzeroFloat32": reflect.ValueOf(constant.MakeFromLiteral("1.40129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158203125e-45", token.FLOAT, 0)), "SmallestNonzeroFloat64": reflect.ValueOf(constant.MakeFromLiteral("4.940656458412465441765687928682213723650598026143247644255856825006755072702087518652998363616359923797965646954457177309266567103559397963987747960107818781263007131903114045278458171678489821036887186360569987307230500063874091535649843873124733972731696151400317153853980741262385655911710266585566867681870395603106249319452715914924553293054565444011274801297099995419319894090804165633245247571478690147267801593552386115501348035264934720193790268107107491703332226844753335720832431936092382893458368060106011506169809753078342277318329247904982524730776375927247874656084778203734469699533647017972677717585125660551199131504891101451037862738167250955837389733598993664809941164205702637090279242767544565229087538682506419718265533447265625e-324", token.FLOAT, 0)), "Sqrt": reflect.ValueOf(math.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(math.Tan), "Tanh": reflect.ValueOf(math.Tanh), "Trunc": reflect.ValueOf(math.Trunc), "Y0": reflect.ValueOf(math.Y0), "Y1": reflect.ValueOf(math.Y1), "Yn": reflect.ValueOf(math.Yn), } } // Code generated by 'yaegi extract path/filepath'. DO NOT EDIT. //go:build go1.22 // +build go1.22 package basesymbols import ( "go/constant" "go/token" "path/filepath" "reflect" ) func init() { Symbols["path/filepath/filepath"] = map[string]reflect.Value{ // function, constant and variable definitions "Abs": reflect.ValueOf(filepath.Abs), "Base": reflect.ValueOf(filepath.Base), "Clean": reflect.ValueOf(filepath.Clean), "Dir": reflect.ValueOf(filepath.Dir), "ErrBadPattern": reflect.ValueOf(&filepath.ErrBadPattern).Elem(), "EvalSymlinks": reflect.ValueOf(filepath.EvalSymlinks), "Ext": reflect.ValueOf(filepath.Ext), "FromSlash": reflect.ValueOf(filepath.FromSlash), "Glob": reflect.ValueOf(filepath.Glob), "HasPrefix": reflect.ValueOf(filepath.HasPrefix), "IsAbs": reflect.ValueOf(filepath.IsAbs), "IsLocal": reflect.ValueOf(filepath.IsLocal), "Join": reflect.ValueOf(filepath.Join), "ListSeparator": reflect.ValueOf(constant.MakeFromLiteral("58", token.INT, 0)), "Localize": reflect.ValueOf(filepath.Localize), "Match": reflect.ValueOf(filepath.Match), "Rel": reflect.ValueOf(filepath.Rel), "Separator": reflect.ValueOf(constant.MakeFromLiteral("47", token.INT, 0)), "SkipAll": reflect.ValueOf(&filepath.SkipAll).Elem(), "SkipDir": reflect.ValueOf(&filepath.SkipDir).Elem(), "Split": reflect.ValueOf(filepath.Split), "SplitList": reflect.ValueOf(filepath.SplitList), "ToSlash": reflect.ValueOf(filepath.ToSlash), "VolumeName": reflect.ValueOf(filepath.VolumeName), "Walk": reflect.ValueOf(filepath.Walk), "WalkDir": reflect.ValueOf(filepath.WalkDir), // type definitions "WalkFunc": reflect.ValueOf((*filepath.WalkFunc)(nil)), } } // Code generated by 'yaegi extract reflect'. DO NOT EDIT. //go:build go1.22 // +build go1.22 package basesymbols 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), "SliceAt": reflect.ValueOf(reflect.SliceAt), "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 WCanSeq func() bool WCanSeq2 func() bool 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 WOverflowComplex func(x complex128) bool WOverflowFloat func(x float64) bool WOverflowInt func(x int64) bool WOverflowUint func(x uint64) bool 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) CanSeq() bool { return W.WCanSeq() } func (W _reflect_Type) CanSeq2() bool { return W.WCanSeq2() } 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) OverflowComplex(x complex128) bool { return W.WOverflowComplex(x) } func (W _reflect_Type) OverflowFloat(x float64) bool { return W.WOverflowFloat(x) } func (W _reflect_Type) OverflowInt(x int64) bool { return W.WOverflowInt(x) } func (W _reflect_Type) OverflowUint(x uint64) bool { return W.WOverflowUint(x) } 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 basesymbols 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 basesymbols 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 basesymbols 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)), } } // Code generated by 'yaegi extract cogentcore.org/core/base/iox/imagex'. DO NOT EDIT. package coresymbols import ( "cogentcore.org/core/base/iox/imagex" "image" "image/color" "reflect" ) func init() { Symbols["cogentcore.org/core/base/iox/imagex/imagex"] = map[string]reflect.Value{ // function, constant and variable definitions "AsRGBA": reflect.ValueOf(imagex.AsRGBA), "Assert": reflect.ValueOf(imagex.Assert), "BMP": reflect.ValueOf(imagex.BMP), "Base64SplitLines": reflect.ValueOf(imagex.Base64SplitLines), "CloneAsRGBA": reflect.ValueOf(imagex.CloneAsRGBA), "CompareColors": reflect.ValueOf(imagex.CompareColors), "CompareUint8": reflect.ValueOf(imagex.CompareUint8), "DiffImage": reflect.ValueOf(imagex.DiffImage), "ExtToFormat": reflect.ValueOf(imagex.ExtToFormat), "FormatsN": reflect.ValueOf(imagex.FormatsN), "FormatsValues": reflect.ValueOf(imagex.FormatsValues), "FromBase64": reflect.ValueOf(imagex.FromBase64), "FromBase64JPG": reflect.ValueOf(imagex.FromBase64JPG), "FromBase64PNG": reflect.ValueOf(imagex.FromBase64PNG), "GIF": reflect.ValueOf(imagex.GIF), "JPEG": reflect.ValueOf(imagex.JPEG), "None": reflect.ValueOf(imagex.None), "Open": reflect.ValueOf(imagex.Open), "OpenFS": reflect.ValueOf(imagex.OpenFS), "PNG": reflect.ValueOf(imagex.PNG), "Read": reflect.ValueOf(imagex.Read), "Save": reflect.ValueOf(imagex.Save), "TIFF": reflect.ValueOf(imagex.TIFF), "ToBase64JPG": reflect.ValueOf(imagex.ToBase64JPG), "ToBase64PNG": reflect.ValueOf(imagex.ToBase64PNG), "Unwrap": reflect.ValueOf(imagex.Unwrap), "Update": reflect.ValueOf(imagex.Update), "UpdateTestImages": reflect.ValueOf(&imagex.UpdateTestImages).Elem(), "WebP": reflect.ValueOf(imagex.WebP), "WrapJS": reflect.ValueOf(imagex.WrapJS), "Write": reflect.ValueOf(imagex.Write), // type definitions "Formats": reflect.ValueOf((*imagex.Formats)(nil)), "TestingT": reflect.ValueOf((*imagex.TestingT)(nil)), "Wrapped": reflect.ValueOf((*imagex.Wrapped)(nil)), // interface wrapper definitions "_TestingT": reflect.ValueOf((*_cogentcore_org_core_base_iox_imagex_TestingT)(nil)), "_Wrapped": reflect.ValueOf((*_cogentcore_org_core_base_iox_imagex_Wrapped)(nil)), } } // _cogentcore_org_core_base_iox_imagex_TestingT is an interface wrapper for TestingT type type _cogentcore_org_core_base_iox_imagex_TestingT struct { IValue interface{} WErrorf func(format string, args ...any) } func (W _cogentcore_org_core_base_iox_imagex_TestingT) Errorf(format string, args ...any) { W.WErrorf(format, args...) } // _cogentcore_org_core_base_iox_imagex_Wrapped is an interface wrapper for Wrapped type type _cogentcore_org_core_base_iox_imagex_Wrapped struct { IValue interface{} WAt func(x int, y int) color.Color WBounds func() image.Rectangle WColorModel func() color.Model WUnderlying func() image.Image WUpdate func() } func (W _cogentcore_org_core_base_iox_imagex_Wrapped) At(x int, y int) color.Color { return W.WAt(x, y) } func (W _cogentcore_org_core_base_iox_imagex_Wrapped) Bounds() image.Rectangle { return W.WBounds() } func (W _cogentcore_org_core_base_iox_imagex_Wrapped) ColorModel() color.Model { return W.WColorModel() } func (W _cogentcore_org_core_base_iox_imagex_Wrapped) Underlying() image.Image { return W.WUnderlying() } func (W _cogentcore_org_core_base_iox_imagex_Wrapped) Update() { W.WUpdate() } // Code generated by 'yaegi extract cogentcore.org/core/colors/gradient'. DO NOT EDIT. package coresymbols 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 coresymbols 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/content'. DO NOT EDIT. package coresymbols import ( "cogentcore.org/core/content" "reflect" ) func init() { Symbols["cogentcore.org/core/content/content"] = map[string]reflect.Value{ // function, constant and variable definitions "NewContent": reflect.ValueOf(content.NewContent), // type definitions "Content": reflect.ValueOf((*content.Content)(nil)), } } // Code generated by 'yaegi extract cogentcore.org/core/core'. DO NOT EDIT. package coresymbols 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/composer" "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), "HighlightingEditor": reflect.ValueOf(core.HighlightingEditor), "InitValueButton": reflect.ValueOf(core.InitValueButton), "InspectorWindow": reflect.ValueOf(core.InspectorWindow), "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), "NewCollapser": reflect.ValueOf(core.NewCollapser), "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), "NewHighlightingButton": reflect.ValueOf(core.NewHighlightingButton), "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), "SceneSource": reflect.ValueOf(core.SceneSource), "ScrimSource": reflect.ValueOf(core.ScrimSource), "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), "SpritesSource": reflect.ValueOf(core.SpritesSource), "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 "Animation": reflect.ValueOf((*core.Animation)(nil)), "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)), "Collapser": reflect.ValueOf((*core.Collapser)(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)), "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)), "HighlightingButton": reflect.ValueOf((*core.HighlightingButton)(nil)), "HighlightingName": reflect.ValueOf((*core.HighlightingName)(nil)), "Icon": reflect.ValueOf((*core.Icon)(nil)), "IconButton": reflect.ValueOf((*core.IconButton)(nil)), "Image": reflect.ValueOf((*core.Image)(nil)), "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)), "Tabber": reflect.ValueOf((*core.Tabber)(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)), "_Tabber": reflect.ValueOf((*_cogentcore_org_core_core_Tabber)(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() WRenderSource func(op draw.Op) composer.Source 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) RenderSource(op draw.Op) composer.Source { return W.WRenderSource(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_Tabber is an interface wrapper for Tabber type type _cogentcore_org_core_core_Tabber struct { IValue interface{} WAsCoreTabs func() *core.Tabs } func (W _cogentcore_org_core_core_Tabber) AsCoreTabs() *core.Tabs { return W.WAsCoreTabs() } // _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() WRenderSource func(op draw.Op) composer.Source 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) RenderSource(op draw.Op) composer.Source { return W.WRenderSource(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() WRenderSource func(op draw.Op) composer.Source 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) RenderSource(op draw.Op) composer.Source { return W.WRenderSource(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() WRenderSource func(op draw.Op) composer.Source 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) RenderSource(op draw.Op) composer.Source { return W.WRenderSource(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 coresymbols 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 "Attend": reflect.ValueOf(events.Attend), "AttendLost": reflect.ValueOf(events.AttendLost), "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 coresymbols import ( "cogentcore.org/core/base/fileinfo/mimedata" "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/filetree" "cogentcore.org/core/system/composer" "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), "AsTree": reflect.ValueOf(filetree.AsTree), "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), // type definitions "DirFlagMap": reflect.ValueOf((*filetree.DirFlagMap)(nil)), "Filer": reflect.ValueOf((*filetree.Filer)(nil)), "Node": reflect.ValueOf((*filetree.Node)(nil)), "NodeEmbedder": reflect.ValueOf((*filetree.NodeEmbedder)(nil)), "NodeNameCount": reflect.ValueOf((*filetree.NodeNameCount)(nil)), "Tree": reflect.ValueOf((*filetree.Tree)(nil)), "Treer": reflect.ValueOf((*filetree.Treer)(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)), "_Treer": reflect.ValueOf((*_cogentcore_org_core_filetree_Treer)(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() WDeleteFiles 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() WRenderSource func(op draw.Op) composer.Source 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) DeleteFiles() { W.WDeleteFiles() } 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) RenderSource(op draw.Op) composer.Source { return W.WRenderSource(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() } // _cogentcore_org_core_filetree_Treer is an interface wrapper for Treer type type _cogentcore_org_core_filetree_Treer struct { IValue interface{} WAsFileTree func() *filetree.Tree } func (W _cogentcore_org_core_filetree_Treer) AsFileTree() *filetree.Tree { return W.WAsFileTree() } // Code generated by 'yaegi extract cogentcore.org/core/htmlcore'. DO NOT EDIT. package coresymbols 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(), "ExtractText": reflect.ValueOf(htmlcore.ExtractText), "Get": reflect.ValueOf(htmlcore.Get), "GetAttr": reflect.ValueOf(htmlcore.GetAttr), "GetURLFromFS": reflect.ValueOf(htmlcore.GetURLFromFS), "GoDocWikilink": reflect.ValueOf(htmlcore.GoDocWikilink), "HasAttr": reflect.ValueOf(htmlcore.HasAttr), "MDGetAttr": reflect.ValueOf(htmlcore.MDGetAttr), "MDSetAttr": reflect.ValueOf(htmlcore.MDSetAttr), "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), // type definitions "Context": reflect.ValueOf((*htmlcore.Context)(nil)), "WikilinkHandler": reflect.ValueOf((*htmlcore.WikilinkHandler)(nil)), } } // Code generated by 'yaegi extract cogentcore.org/core/keymap'. DO NOT EDIT. package coresymbols 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/paint'. DO NOT EDIT. package coresymbols 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), "NewImageRenderer": reflect.ValueOf(&paint.NewImageRenderer).Elem(), "NewPainter": reflect.ValueOf(paint.NewPainter), "NewSVGRenderer": reflect.ValueOf(&paint.NewSVGRenderer).Elem(), "NewSourceRenderer": reflect.ValueOf(&paint.NewSourceRenderer).Elem(), "RenderToImage": reflect.ValueOf(paint.RenderToImage), "RenderToSVG": reflect.ValueOf(paint.RenderToSVG), // type definitions "Painter": reflect.ValueOf((*paint.Painter)(nil)), "State": reflect.ValueOf((*paint.State)(nil)), } } // Code generated by 'yaegi extract cogentcore.org/core/styles/abilities'. DO NOT EDIT. package coresymbols 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), "Pressable": reflect.ValueOf(&abilities.Pressable).Elem(), "RepeatClickable": reflect.ValueOf(abilities.RepeatClickable), "Scrollable": reflect.ValueOf(abilities.Scrollable), "ScrollableUnattended": reflect.ValueOf(abilities.ScrollableUnattended), "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 coresymbols 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), "Attended": reflect.ValueOf(states.Attended), "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 coresymbols 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 coresymbols 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), "Auto": reflect.ValueOf(styles.Auto), "Baseline": reflect.ValueOf(styles.Baseline), "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), "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), "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), "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), "Flex": reflect.ValueOf(styles.Flex), "FontSizePoints": reflect.ValueOf(&styles.FontSizePoints).Elem(), "Grid": reflect.ValueOf(styles.Grid), "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), "NewPaint": reflect.ValueOf(styles.NewPaint), "NewPaintWithContext": reflect.ValueOf(styles.NewPaintWithContext), "NewStyle": reflect.ValueOf(styles.NewStyle), "ObjectFitsN": reflect.ValueOf(styles.ObjectFitsN), "ObjectFitsValues": reflect.ValueOf(styles.ObjectFitsValues), "ObjectSizeFromFit": reflect.ValueOf(styles.ObjectSizeFromFit), "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), "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), "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(), "SubProperties": reflect.ValueOf(styles.SubProperties), "ToCSS": reflect.ValueOf(styles.ToCSS), "VirtualKeyboardsN": reflect.ValueOf(styles.VirtualKeyboardsN), "VirtualKeyboardsValues": reflect.ValueOf(styles.VirtualKeyboardsValues), // type definitions "AlignSet": reflect.ValueOf((*styles.AlignSet)(nil)), "Aligns": reflect.ValueOf((*styles.Aligns)(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)), "Font": reflect.ValueOf((*styles.Font)(nil)), "ObjectFits": reflect.ValueOf((*styles.ObjectFits)(nil)), "Overflows": reflect.ValueOf((*styles.Overflows)(nil)), "Paint": reflect.ValueOf((*styles.Paint)(nil)), "Path": reflect.ValueOf((*styles.Path)(nil)), "Shadow": reflect.ValueOf((*styles.Shadow)(nil)), "Stroke": reflect.ValueOf((*styles.Stroke)(nil)), "Style": reflect.ValueOf((*styles.Style)(nil)), "Text": reflect.ValueOf((*styles.Text)(nil)), "VirtualKeyboards": reflect.ValueOf((*styles.VirtualKeyboards)(nil)), } } // Code generated by 'yaegi extract cogentcore.org/core/text/lines'. DO NOT EDIT. package coresymbols import ( "cogentcore.org/core/text/lines" "reflect" ) func init() { Symbols["cogentcore.org/core/text/lines/lines"] = map[string]reflect.Value{ // function, constant and variable definitions "ApplyOneDiff": reflect.ValueOf(lines.ApplyOneDiff), "BytesToLineStrings": reflect.ValueOf(lines.BytesToLineStrings), "CountWordsLines": reflect.ValueOf(lines.CountWordsLines), "CountWordsLinesRegion": reflect.ValueOf(lines.CountWordsLinesRegion), "DiffLines": reflect.ValueOf(lines.DiffLines), "DiffLinesUnified": reflect.ValueOf(lines.DiffLinesUnified), "DiffOpReverse": reflect.ValueOf(lines.DiffOpReverse), "DiffOpString": reflect.ValueOf(lines.DiffOpString), "FileBytes": reflect.ValueOf(lines.FileBytes), "FileRegionBytes": reflect.ValueOf(lines.FileRegionBytes), "KnownComments": reflect.ValueOf(lines.KnownComments), "NewDiffSelected": reflect.ValueOf(lines.NewDiffSelected), "NewLines": reflect.ValueOf(lines.NewLines), "NewLinesFromBytes": reflect.ValueOf(lines.NewLinesFromBytes), "NextSpace": reflect.ValueOf(lines.NextSpace), "PreCommentStart": reflect.ValueOf(lines.PreCommentStart), "ReplaceMatchCase": reflect.ValueOf(lines.ReplaceMatchCase), "ReplaceNoMatchCase": reflect.ValueOf(lines.ReplaceNoMatchCase), "StringLinesToByteLines": reflect.ValueOf(lines.StringLinesToByteLines), "UndoGroupDelay": reflect.ValueOf(&lines.UndoGroupDelay).Elem(), "UndoTrace": reflect.ValueOf(&lines.UndoTrace).Elem(), // type definitions "DiffSelectData": reflect.ValueOf((*lines.DiffSelectData)(nil)), "DiffSelected": reflect.ValueOf((*lines.DiffSelected)(nil)), "Diffs": reflect.ValueOf((*lines.Diffs)(nil)), "Lines": reflect.ValueOf((*lines.Lines)(nil)), "Patch": reflect.ValueOf((*lines.Patch)(nil)), "PatchRec": reflect.ValueOf((*lines.PatchRec)(nil)), "Settings": reflect.ValueOf((*lines.Settings)(nil)), "Undo": reflect.ValueOf((*lines.Undo)(nil)), } } // Code generated by 'yaegi extract cogentcore.org/core/text/rich'. DO NOT EDIT. package coresymbols import ( "cogentcore.org/core/text/rich" "go/constant" "go/token" "reflect" ) func init() { Symbols["cogentcore.org/core/text/rich/rich"] = map[string]reflect.Value{ // function, constant and variable definitions "AddFamily": reflect.ValueOf(rich.AddFamily), "BTT": reflect.ValueOf(rich.BTT), "Background": reflect.ValueOf(rich.Background), "Black": reflect.ValueOf(rich.Black), "Bold": reflect.ValueOf(rich.Bold), "ColorFromRune": reflect.ValueOf(rich.ColorFromRune), "ColorToRune": reflect.ValueOf(rich.ColorToRune), "Condensed": reflect.ValueOf(rich.Condensed), "Cursive": reflect.ValueOf(rich.Cursive), "Custom": reflect.ValueOf(rich.Custom), "DecorationMask": reflect.ValueOf(constant.MakeFromLiteral("2047", token.INT, 0)), "DecorationStart": reflect.ValueOf(constant.MakeFromLiteral("0", token.INT, 0)), "DecorationsN": reflect.ValueOf(rich.DecorationsN), "DecorationsValues": reflect.ValueOf(rich.DecorationsValues), "Default": reflect.ValueOf(rich.Default), "DefaultSettings": reflect.ValueOf(&rich.DefaultSettings).Elem(), "DirectionMask": reflect.ValueOf(constant.MakeFromLiteral("4026531840", token.INT, 0)), "DirectionStart": reflect.ValueOf(constant.MakeFromLiteral("28", token.INT, 0)), "DirectionsN": reflect.ValueOf(rich.DirectionsN), "DirectionsValues": reflect.ValueOf(rich.DirectionsValues), "DottedUnderline": reflect.ValueOf(rich.DottedUnderline), "Emoji": reflect.ValueOf(rich.Emoji), "End": reflect.ValueOf(rich.End), "Expanded": reflect.ValueOf(rich.Expanded), "ExtraBold": reflect.ValueOf(rich.ExtraBold), "ExtraCondensed": reflect.ValueOf(rich.ExtraCondensed), "ExtraExpanded": reflect.ValueOf(rich.ExtraExpanded), "ExtraLight": reflect.ValueOf(rich.ExtraLight), "FamiliesToList": reflect.ValueOf(rich.FamiliesToList), "FamilyMask": reflect.ValueOf(constant.MakeFromLiteral("251658240", token.INT, 0)), "FamilyN": reflect.ValueOf(rich.FamilyN), "FamilyStart": reflect.ValueOf(constant.MakeFromLiteral("24", token.INT, 0)), "FamilyValues": reflect.ValueOf(rich.FamilyValues), "Fangsong": reflect.ValueOf(rich.Fangsong), "Fantasy": reflect.ValueOf(rich.Fantasy), "FillColor": reflect.ValueOf(rich.FillColor), "FontSizes": reflect.ValueOf(&rich.FontSizes).Elem(), "Italic": reflect.ValueOf(rich.Italic), "Join": reflect.ValueOf(rich.Join), "LTR": reflect.ValueOf(rich.LTR), "Light": reflect.ValueOf(rich.Light), "LineThrough": reflect.ValueOf(rich.LineThrough), "Link": reflect.ValueOf(rich.Link), "Math": reflect.ValueOf(rich.Math), "MathDisplay": reflect.ValueOf(rich.MathDisplay), "MathInline": reflect.ValueOf(rich.MathInline), "Medium": reflect.ValueOf(rich.Medium), "Monospace": reflect.ValueOf(rich.Monospace), "NewPlainText": reflect.ValueOf(rich.NewPlainText), "NewStyle": reflect.ValueOf(rich.NewStyle), "NewStyleFromRunes": reflect.ValueOf(rich.NewStyleFromRunes), "NewText": reflect.ValueOf(rich.NewText), "Normal": reflect.ValueOf(rich.Normal), "Nothing": reflect.ValueOf(rich.Nothing), "Overline": reflect.ValueOf(rich.Overline), "ParagraphStart": reflect.ValueOf(rich.ParagraphStart), "Quote": reflect.ValueOf(rich.Quote), "RTL": reflect.ValueOf(rich.RTL), "RuneFromDecoration": reflect.ValueOf(rich.RuneFromDecoration), "RuneFromDirection": reflect.ValueOf(rich.RuneFromDirection), "RuneFromFamily": reflect.ValueOf(rich.RuneFromFamily), "RuneFromSlant": reflect.ValueOf(rich.RuneFromSlant), "RuneFromSpecial": reflect.ValueOf(rich.RuneFromSpecial), "RuneFromStretch": reflect.ValueOf(rich.RuneFromStretch), "RuneFromStyle": reflect.ValueOf(rich.RuneFromStyle), "RuneFromWeight": reflect.ValueOf(rich.RuneFromWeight), "RuneToDecoration": reflect.ValueOf(rich.RuneToDecoration), "RuneToDirection": reflect.ValueOf(rich.RuneToDirection), "RuneToFamily": reflect.ValueOf(rich.RuneToFamily), "RuneToSlant": reflect.ValueOf(rich.RuneToSlant), "RuneToSpecial": reflect.ValueOf(rich.RuneToSpecial), "RuneToStretch": reflect.ValueOf(rich.RuneToStretch), "RuneToStyle": reflect.ValueOf(rich.RuneToStyle), "RuneToWeight": reflect.ValueOf(rich.RuneToWeight), "SansSerif": reflect.ValueOf(rich.SansSerif), "SemiCondensed": reflect.ValueOf(rich.SemiCondensed), "SemiExpanded": reflect.ValueOf(rich.SemiExpanded), "Semibold": reflect.ValueOf(rich.Semibold), "Serif": reflect.ValueOf(rich.Serif), "SlantMask": reflect.ValueOf(constant.MakeFromLiteral("2048", token.INT, 0)), "SlantNormal": reflect.ValueOf(rich.SlantNormal), "SlantStart": reflect.ValueOf(constant.MakeFromLiteral("11", token.INT, 0)), "SlantsN": reflect.ValueOf(rich.SlantsN), "SlantsValues": reflect.ValueOf(rich.SlantsValues), "SpanLen": reflect.ValueOf(rich.SpanLen), "SpecialMask": reflect.ValueOf(constant.MakeFromLiteral("61440", token.INT, 0)), "SpecialStart": reflect.ValueOf(constant.MakeFromLiteral("12", token.INT, 0)), "SpecialsN": reflect.ValueOf(rich.SpecialsN), "SpecialsValues": reflect.ValueOf(rich.SpecialsValues), "StretchMask": reflect.ValueOf(constant.MakeFromLiteral("983040", token.INT, 0)), "StretchN": reflect.ValueOf(rich.StretchN), "StretchNormal": reflect.ValueOf(rich.StretchNormal), "StretchStart": reflect.ValueOf(constant.MakeFromLiteral("16", token.INT, 0)), "StretchValues": reflect.ValueOf(rich.StretchValues), "StrokeColor": reflect.ValueOf(rich.StrokeColor), "Sub": reflect.ValueOf(rich.Sub), "Super": reflect.ValueOf(rich.Super), "TTB": reflect.ValueOf(rich.TTB), "Thin": reflect.ValueOf(rich.Thin), "UltraCondensed": reflect.ValueOf(rich.UltraCondensed), "UltraExpanded": reflect.ValueOf(rich.UltraExpanded), "Underline": reflect.ValueOf(rich.Underline), "WeightMask": reflect.ValueOf(constant.MakeFromLiteral("15728640", token.INT, 0)), "WeightStart": reflect.ValueOf(constant.MakeFromLiteral("20", token.INT, 0)), "WeightsN": reflect.ValueOf(rich.WeightsN), "WeightsValues": reflect.ValueOf(rich.WeightsValues), // type definitions "Decorations": reflect.ValueOf((*rich.Decorations)(nil)), "Directions": reflect.ValueOf((*rich.Directions)(nil)), "Family": reflect.ValueOf((*rich.Family)(nil)), "FontName": reflect.ValueOf((*rich.FontName)(nil)), "Hyperlink": reflect.ValueOf((*rich.Hyperlink)(nil)), "Settings": reflect.ValueOf((*rich.Settings)(nil)), "Slants": reflect.ValueOf((*rich.Slants)(nil)), "Specials": reflect.ValueOf((*rich.Specials)(nil)), "Stretch": reflect.ValueOf((*rich.Stretch)(nil)), "Style": reflect.ValueOf((*rich.Style)(nil)), "Text": reflect.ValueOf((*rich.Text)(nil)), "Weights": reflect.ValueOf((*rich.Weights)(nil)), } } // Code generated by 'yaegi extract cogentcore.org/core/text/text'. DO NOT EDIT. package coresymbols import ( "cogentcore.org/core/text/text" "reflect" ) func init() { Symbols["cogentcore.org/core/text/text/text"] = map[string]reflect.Value{ // function, constant and variable definitions "AlignsN": reflect.ValueOf(text.AlignsN), "AlignsValues": reflect.ValueOf(text.AlignsValues), "Center": reflect.ValueOf(text.Center), "End": reflect.ValueOf(text.End), "Justify": reflect.ValueOf(text.Justify), "NewFont": reflect.ValueOf(text.NewFont), "NewStyle": reflect.ValueOf(text.NewStyle), "Start": reflect.ValueOf(text.Start), "WhiteSpacePre": reflect.ValueOf(text.WhiteSpacePre), "WhiteSpacePreWrap": reflect.ValueOf(text.WhiteSpacePreWrap), "WhiteSpacesN": reflect.ValueOf(text.WhiteSpacesN), "WhiteSpacesValues": reflect.ValueOf(text.WhiteSpacesValues), "WrapAlways": reflect.ValueOf(text.WrapAlways), "WrapAsNeeded": reflect.ValueOf(text.WrapAsNeeded), "WrapNever": reflect.ValueOf(text.WrapNever), "WrapSpaceOnly": reflect.ValueOf(text.WrapSpaceOnly), // type definitions "Aligns": reflect.ValueOf((*text.Aligns)(nil)), "EditorSettings": reflect.ValueOf((*text.EditorSettings)(nil)), "Font": reflect.ValueOf((*text.Font)(nil)), "Style": reflect.ValueOf((*text.Style)(nil)), "WhiteSpaces": reflect.ValueOf((*text.WhiteSpaces)(nil)), } } // Code generated by 'yaegi extract cogentcore.org/core/text/textcore'. DO NOT EDIT. package coresymbols import ( "cogentcore.org/core/text/textcore" "reflect" ) func init() { Symbols["cogentcore.org/core/text/textcore/textcore"] = map[string]reflect.Value{ // function, constant and variable definitions "AsBase": reflect.ValueOf(textcore.AsBase), "AsEditor": reflect.ValueOf(textcore.AsEditor), "Close": reflect.ValueOf(textcore.Close), "DiffEditorDialog": reflect.ValueOf(textcore.DiffEditorDialog), "DiffEditorDialogFromRevs": reflect.ValueOf(textcore.DiffEditorDialogFromRevs), "DiffFiles": reflect.ValueOf(textcore.DiffFiles), "FileModPrompt": reflect.ValueOf(textcore.FileModPrompt), "NewBase": reflect.ValueOf(textcore.NewBase), "NewDiffEditor": reflect.ValueOf(textcore.NewDiffEditor), "NewDiffTextEditor": reflect.ValueOf(textcore.NewDiffTextEditor), "NewEditor": reflect.ValueOf(textcore.NewEditor), "NewTwinEditors": reflect.ValueOf(textcore.NewTwinEditors), "PrevISearchString": reflect.ValueOf(&textcore.PrevISearchString).Elem(), "Save": reflect.ValueOf(textcore.Save), "SaveAs": reflect.ValueOf(textcore.SaveAs), "TextDialog": reflect.ValueOf(textcore.TextDialog), // type definitions "Base": reflect.ValueOf((*textcore.Base)(nil)), "BaseEmbedder": reflect.ValueOf((*textcore.BaseEmbedder)(nil)), "DiffEditor": reflect.ValueOf((*textcore.DiffEditor)(nil)), "DiffTextEditor": reflect.ValueOf((*textcore.DiffTextEditor)(nil)), "Editor": reflect.ValueOf((*textcore.Editor)(nil)), "EditorEmbedder": reflect.ValueOf((*textcore.EditorEmbedder)(nil)), "ISearch": reflect.ValueOf((*textcore.ISearch)(nil)), "OutputBuffer": reflect.ValueOf((*textcore.OutputBuffer)(nil)), "OutputBufferMarkupFunc": reflect.ValueOf((*textcore.OutputBufferMarkupFunc)(nil)), "QReplace": reflect.ValueOf((*textcore.QReplace)(nil)), "TwinEditors": reflect.ValueOf((*textcore.TwinEditors)(nil)), // interface wrapper definitions "_BaseEmbedder": reflect.ValueOf((*_cogentcore_org_core_text_textcore_BaseEmbedder)(nil)), "_EditorEmbedder": reflect.ValueOf((*_cogentcore_org_core_text_textcore_EditorEmbedder)(nil)), } } // _cogentcore_org_core_text_textcore_BaseEmbedder is an interface wrapper for BaseEmbedder type type _cogentcore_org_core_text_textcore_BaseEmbedder struct { IValue interface{} WAsBase func() *textcore.Base } func (W _cogentcore_org_core_text_textcore_BaseEmbedder) AsBase() *textcore.Base { return W.WAsBase() } // _cogentcore_org_core_text_textcore_EditorEmbedder is an interface wrapper for EditorEmbedder type type _cogentcore_org_core_text_textcore_EditorEmbedder struct { IValue interface{} WAsEditor func() *textcore.Editor } func (W _cogentcore_org_core_text_textcore_EditorEmbedder) AsEditor() *textcore.Editor { return W.WAsEditor() } // Code generated by 'yaegi extract cogentcore.org/core/tree'. DO NOT EDIT. package coresymbols 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), "IsNil": reflect.ValueOf(tree.IsNil), "IsNode": reflect.ValueOf(tree.IsNode), "IsRoot": reflect.ValueOf(tree.IsRoot), "Last": reflect.ValueOf(tree.Last), "MoveToParent": reflect.ValueOf(tree.MoveToParent), "New": reflect.ValueOf(interp.GenericFunc("func New[T NodeValue](parent ...Node) *T { //yaegi:add\n\tn := new(T)\n\tni := any(n).(Node)\n\tInitNode(ni)\n\tif len(parent) == 0 {\n\t\tni.AsTree().SetName(ni.AsTree().NodeType().IDName)\n\t\treturn n\n\t}\n\tp := parent[0]\n\tp.AsTree().Children = append(p.AsTree().Children, ni)\n\tSetParent(ni, p)\n\treturn n\n}")), "NewNodeBase": reflect.ValueOf(tree.NewNodeBase), "NewOfType": reflect.ValueOf(tree.NewOfType), "Next": reflect.ValueOf(tree.Next), "NextSibling": reflect.ValueOf(tree.NextSibling), "Previous": reflect.ValueOf(tree.Previous), "Root": reflect.ValueOf(tree.Root), "SetParent": reflect.ValueOf(tree.SetParent), "SetUniqueName": reflect.ValueOf(tree.SetUniqueName), "UnescapePathName": reflect.ValueOf(tree.UnescapePathName), "UnmarshalRootJSON": reflect.ValueOf(tree.UnmarshalRootJSON), "Update": reflect.ValueOf(tree.Update), "UpdateSlice": reflect.ValueOf(tree.UpdateSlice), // 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() } // Copyright (c) 2025, Cogent Core. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package coresymbols import ( "reflect" . "cogentcore.org/core/icons" ) // iconsList is a subset of icons to include in the yaegi symbols. // It is based on the icons used in the core docs. var iconsList = map[string]Icon{"Download": Download, "Share": Share, "Send": Send, "Computer": Computer, "Smartphone": Smartphone, "Sort": Sort, "Home": Home, "HomeFill": HomeFill, "DeployedCodeFill": DeployedCodeFill, "Close": Close, "Explore": Explore, "History": History, "Euro": Euro, "OpenInNew": OpenInNew, "Add": Add} func init() { m := map[string]reflect.Value{} for name, icon := range iconsList { m[name] = reflect.ValueOf(icon) } Symbols["cogentcore.org/core/icons/icons"] = m } // Code generated by 'yaegi extract image/color'. DO NOT EDIT. //go:build go1.22 // +build go1.22 package coresymbols 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 coresymbols 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 coresymbols 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) } // 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/text/textcore" "cogentcore.org/core/yaegicore/basesymbols" "cogentcore.org/core/yaegicore/coresymbols" "github.com/cogentcore/yaegi/interp" ) // Interpreters is a map from language names (such as "Go") to functions that create a // new [Interpreter] for that language. The base implementation is just [interp.Interpreter] // for Go, but other packages can extend this. See the [Interpreter] interface for more information. var Interpreters = map[string]func(options interp.Options) Interpreter{ "Go": func(options interp.Options) Interpreter { return interp.New(options) }, } // Interpreter is an interface that represents the functionality provided by an interpreter // compatible with yaegicore. The base implementation is just [interp.Interpreter], but other // packages such as yaegilab in Cogent Lab provide their own implementations with other languages // such as Cogent Goal. See [Interpreters]. type Interpreter interface { // Use imports the given symbols into the interpreter. Use(values interp.Exports) error // ImportUsed imports the used symbols into the interpreter // and does any extra necessary configuration steps. ImportUsed() // Eval evaluates the given code in the interpreter. Eval(src string) (res reflect.Value, err error) } var autoPlanNameCounter uint64 func init() { htmlcore.BindTextEditor = BindTextEditor coresymbols.Symbols["."] = map[string]reflect.Value{} // make "." available for use basesymbols.Symbols["."] = map[string]reflect.Value{} // make "." available for use } var currentGoalInterpreter Interpreter // interpreterParent is used to store the parent widget ("b") for the interpreter. // It exists (as a double pointer) such that it can be updated after-the-fact, such // as in Cogent Lab/Goal where interpreters are re-used across multiple text editors, // wherein the parent widget must be remotely controllable with a double pointer to // keep the parent widget up-to-date. var interpreterParent = new(*core.Frame) // getInterpreter returns a new interpreter for the given language, // or [currentGoalInterpreter] if the language is "Goal" and it is non-nil. func getInterpreter(language string) (in Interpreter, new bool, err error) { if language == "Goal" && currentGoalInterpreter != nil { return currentGoalInterpreter, false, nil } f := Interpreters[language] if f == nil { return nil, false, fmt.Errorf("no entry in yaegicore.Interpreters for language %q", language) } in = f(interp.Options{}) if language == "Goal" { currentGoalInterpreter = in } return in, true, nil } // BindTextEditor binds the given text editor to a yaegi interpreter // such that the contents of the text editor are interpreted as code // of the given language, which is run in the context of the given parent widget. // It is used as the default value of [htmlcore.BindTextEditor]. func BindTextEditor(ed *textcore.Editor, parent *core.Frame, language string) { oc := func() { in, new, err := getInterpreter(language) if err != nil { core.ErrorSnackbar(ed, err) return } core.ExternalParent = parent *interpreterParent = parent coresymbols.Symbols["."]["b"] = reflect.ValueOf(interpreterParent).Elem() // the normal AutoPlanName cannot be used because the stack trace in yaegi is not helpful coresymbols.Symbols["cogentcore.org/core/tree/tree"]["AutoPlanName"] = reflect.ValueOf(func(int) string { return fmt.Sprintf("yaegi-%v", atomic.AddUint64(&autoPlanNameCounter, 1)) }) if new { errors.Log(in.Use(basesymbols.Symbols)) errors.Log(in.Use(coresymbols.Symbols)) in.ImportUsed() } parent.DeleteChildren() str := ed.Lines.String() // all Go code must be in a function for declarations to be handled correctly if language == "Go" && !strings.Contains(str, "func main()") { str = "func main() {\n" + str + "\n}" } _, err = in.Eval(str) if err != nil { core.ErrorSnackbar(ed, err, fmt.Sprintf("Error interpreting %s code", language)) return } parent.Update() } ed.OnChange(func(e events.Event) { oc() }) oc() }