diff options
Diffstat (limited to 'vrsite/css.go')
| -rw-r--r-- | vrsite/css.go | 171 |
1 files changed, 130 insertions, 41 deletions
diff --git a/vrsite/css.go b/vrsite/css.go index a2ca377..f1df936 100644 --- a/vrsite/css.go +++ b/vrsite/css.go @@ -1,68 +1,157 @@ -// css.go — the stylesheet is generated per site: theme colors and the mixed -// per-agent coverage colors become custom properties, one block per scheme. +// css.go — the stylesheet is generated per site: theme colors become custom +// properties, one block per scheme, and the per-agent coverage colors are +// blended from them in the browser so any theme can supply them. +// +// Everything switchable is a custom property on :root. That is what lets +// themes.css hold nothing but one block per theme (writeThemesCSS) and the +// picker repaint the page by setting a single attribute. package main import ( + "encoding/json" "fmt" "io" - "sort" "strings" texttemplate "text/template" ) type cssVar struct{ K, V string } -type synCSS struct{ Class, Style, Weight string } +type synCSS struct{ Class string } + +type maskCSS struct { + Mask uint8 + Mix string +} func renderCSS(w io.Writer, site *Site) error { css, err := texttemplate.ParseFS(tfs, "assets/style.css.tmpl") if err != nil { return err } - var unionKeys []string - seen := map[string]bool{} - for _, t := range []*Theme{site.Theme, site.Light} { - for k := range t.Syntax { - if !seen[k] { - seen[k] = true - unionKeys = append(unionKeys, k) - } + keys := syntaxKeys(site.Theme, site.Light) + var syn []synCSS + for _, k := range keys { + syn = append(syn, synCSS{Class: cssKey(k)}) + } + var masks []maskCSS + for _, m := range site.masks() { + masks = append(masks, maskCSS{Mask: m, Mix: site.maskMix(m)}) + } + return css.Execute(w, map[string]any{ + "DarkName": site.Theme.Name, "LightName": site.Light.Name, + "DarkVars": themeVars(site.Theme, keys), "LightVars": themeVars(site.Light, keys), + "Syntax": syn, "Masks": masks, + }) +} + +// themeVars is one theme as custom properties: its own colors, the syntax +// colors under the keys the site paints with (resolved along their dots, so a +// theme that only names "punctuation" still colors brackets), and the agent +// colors every coverage blend is mixed from. +func themeVars(t *Theme, keys []string) []cssVar { + // bg and panel are what everything else is read against, so they are the + // two that may not carry a theme's alpha channel through + vars := []cssVar{ + {"--bg", hex6(t.Bg)}, {"--fg", t.Fg}, {"--panel", hex6(t.Panel)}, {"--border", t.Border}, + {"--ln", t.LineNo}, {"--muted", t.Muted}, {"--accent", t.Accent}, + {"--note", t.Note}, {"--hover", t.Hover}, + } + for _, k := range keys { + key := resolveKey(t, k) + if key == "" { + continue + } + s, c := t.Syntax[key], cssKey(k) + vars = append(vars, cssVar{"--s-" + c, s.Color}) + if s.FontStyle != nil && *s.FontStyle != "" && *s.FontStyle != "normal" { + vars = append(vars, cssVar{"--s-" + c + "-fs", *s.FontStyle}) + } + if s.FontWeight != nil && *s.FontWeight > 0 { + vars = append(vars, cssVar{"--s-" + c + "-fw", fmt.Sprintf("%.0f", *s.FontWeight)}) } } - sort.Strings(unionKeys) - masks := site.masks() - themeVars := func(t *Theme) []cssVar { - vars := []cssVar{ - {"--bg", t.Bg}, {"--fg", t.Fg}, {"--panel", t.Panel}, {"--border", t.Border}, - {"--ln", t.LineNo}, {"--muted", t.Muted}, {"--accent", t.Accent}, - {"--note", t.Note}, {"--hover", t.Hover}, + for i := 0; i < maxAgents; i++ { + vars = append(vars, cssVar{fmt.Sprintf("--ag%d", i), hex6(t.Player(i))}) + } + return vars +} + +// maskMix is the color of a coverage bitmask: the agent colors it stands for, +// averaged. The average is written as nested color-mix rather than mixed here, +// so the same rule holds for every theme the browser can switch to. +func (s *Site) maskMix(mask uint8) string { + var idx []int + for i, a := range s.Agents { + if a.Bit != 0 && mask&a.Bit != 0 { + idx = append(idx, i) } - for _, k := range unionKeys { - if s, ok := t.Syntax[k]; ok && s.Color != "" { - vars = append(vars, cssVar{"--s-" + strings.ReplaceAll(k, ".", "-"), s.Color}) - } + } + return mixExpr(idx) +} + +func mixExpr(idx []int) string { + switch len(idx) { + case 0: + return "var(--fg)" + case 1: + return fmt.Sprintf("var(--ag%d)", idx[0]) + } + return fmt.Sprintf("color-mix(in srgb, var(--ag%d) %.3f%%, %s)", + idx[0], 100/float64(len(idx)), mixExpr(idx[1:])) +} + +// ── the switchable themes ───────────────────────────────────────────────── + +// writeThemesCSS writes one block per theme found on this machine. It is only +// fetched once a theme has been picked (or the picker opened), so the page +// costs nothing for the reader who never switches. +func writeThemesCSS(w io.Writer, themes []*Theme, keys []string) { + fmt.Fprintf(w, "/* %d themes, found on this machine — set data-theme on :root to wear one */\n", len(themes)) + for _, t := range themes { + fmt.Fprintf(w, ":root[data-theme=%q] {\n", t.ID) + for _, v := range themeVars(t, keys) { + fmt.Fprintf(w, " %s: %s;\n", v.K, v.V) } - for _, m := range masks { - vars = append(vars, cssVar{fmt.Sprintf("--am%d", m), site.maskColor(t, m)}) + scheme := "dark" + if t.Appearance == "light" { + scheme = "light" } - return vars + fmt.Fprintf(w, " color-scheme: %s;\n}\n", scheme) } - var syn []synCSS - for _, k := range unionKeys { - s := synCSS{Class: strings.ReplaceAll(k, ".", "-")} - if v, ok := site.Theme.Syntax[k]; ok { - if v.FontStyle != nil { - s.Style = *v.FontStyle - } - if v.FontWeight != nil { - s.Weight = fmt.Sprintf("%.0f", *v.FontWeight) +} + +// themeCard is a theme as the picker shows it: a name, where it came from, and +// the few colors that make it recognizable at a glance. +type themeCard struct { + ID string `json:"id"` + Name string `json:"name"` + Src string `json:"src"` + Dark bool `json:"dark"` + Sw []string `json:"sw"` +} + +// writeThemesJS is the picker's menu. It is a script, not JSON, because the +// static export is opened from the filesystem, where fetch() is not allowed +// but a <script src> still loads. +func writeThemesJS(w io.Writer, themes []*Theme) { + cards := make([]themeCard, 0, len(themes)) + for _, t := range themes { + syn := func(key string) string { + if k := resolveKey(t, key); k != "" { + return hex6(t.Syntax[k].Color) } + return hex6(t.Fg) } - syn = append(syn, s) + cards = append(cards, themeCard{ID: t.ID, Name: t.Name, Src: t.Source, + Dark: t.Appearance != "light", + Sw: []string{hex6(t.Bg), hex6(t.Fg), hex6(t.Accent), + syn("keyword"), syn("string"), syn("function")}}) } - return css.Execute(w, map[string]any{ - "DarkName": site.Theme.Name, "LightName": site.Light.Name, - "DarkVars": themeVars(site.Theme), "LightVars": themeVars(site.Light), - "Syntax": syn, "MaskList": masks, - }) + b, err := json.Marshal(cards) + if err != nil { + b = []byte("[]") + } + fmt.Fprintf(w, "// %d themes, written by vrsite from this machine's zed and helix installs\n", len(cards)) + fmt.Fprintf(w, "window.VRSITE_THEMES=%s;\n", strings.ReplaceAll(string(b), "},{", "},\n{")) } |
