summaryrefslogtreecommitdiff
path: root/vrsite/css.go
diff options
context:
space:
mode:
Diffstat (limited to 'vrsite/css.go')
-rw-r--r--vrsite/css.go171
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{"))
}