// Copyright (c) 2023, Cogent Core. 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()
}