diff options
Diffstat (limited to 'scripts/theme-studio/app-core.js')
| -rw-r--r-- | scripts/theme-studio/app-core.js | 109 |
1 files changed, 85 insertions, 24 deletions
diff --git a/scripts/theme-studio/app-core.js b/scripts/theme-studio/app-core.js index 566e5a69b..e74b0835e 100644 --- a/scripts/theme-studio/app-core.js +++ b/scripts/theme-studio/app-core.js @@ -9,7 +9,7 @@ // where normHex (app-util.js) and the colormath helpers are already present from // the bodies inlined above this one. import { normHex } from './app-util.js'; -import { oklch2hex, srgb2oklab, oklab2oklch, oklab2lrgb, lrgb2hex, inGamut, contrast } from './colormath.js'; +import { oklch2hex, srgb2oklab, oklab2lrgb, lrgb2hex, inGamut, contrast, oklchOf, isPureEndpointHex, reliefColors } from './colormath.js'; // Resolve a palette name (or a raw #hex) to a hex; null when the name is unknown. function nameToHex(n,palette){if(!n)return null;if(/^#/.test(n))return n;const p=palette.find(p=>p[1]===n);return p?p[0]:null;} @@ -28,10 +28,80 @@ function migrateLegacyFace(d){ return out; } +// --- face CSS rendering ------------------------------------------------------ +// Pure builders for the face preview/inline CSS strings. app.js's syntaxStyle / +// uiCss / ofs / udeco wrappers differ only in how they resolve fg/bg and whether +// they add a font-size; they all delegate here. cssWeight maps the curated weight +// names to numeric CSS weights; faceDecoration is the underline/strike value. +function cssWeight(w){const M={light:300,normal:400,medium:500,semibold:600,bold:700,heavy:900};return w&&M[w]!=null?M[w]:'normal';} +function faceDecoration(face){return ((face.underline?'underline ':'')+(face.strike?'line-through':'')).trim()||'none';} +// A face's :box, rendered as an inset box-shadow (no layout shift). Returns the +// box-shadow VALUE (or '' for no box). 'line' is a flat border in the box color +// (or the face's own color when unset); 'released'/'pressed' are the 3D button +// styles Emacs draws, derived from explicit box color when set, otherwise BG so +// they read on any color (reliefColors is ported from xterm.c). +function boxCss(b,bg){if(!b||!b.style)return '';const w=b.width||1; + if(b.style==='released'||b.style==='pressed'){ + const r=(b.color||bg)?reliefColors(b.color||bg):{hl:null,sh:null}; + const hl=r.hl||'#ffffff33',sh=r.sh||'#00000066'; + const [a,z]=b.style==='released'?[hl,sh]:[sh,hl]; + return `inset ${w}px ${w}px 0 ${a},inset -${w}px -${w}px 0 ${z}`;} + return `inset 0 0 0 ${w}px ${b.color||'currentColor'}`;} +// CSS declaration string for FACE with already-resolved FG/BG. opts: noBg +// (never emit background), fontSize (em number for height), boxBg (background +// handed to the relief shading). Declaration order matches the strings the four +// callers previously assembled by hand, so the rendered output is unchanged. +function faceCss(face,fg,bg,opts){ + opts=opts||{}; + const parts=['color:'+fg]; + if(bg&&!opts.noBg)parts.push('background:'+bg); + parts.push('font-weight:'+cssWeight(face.weight), + 'font-style:'+(face.slant||'normal'), + 'text-decoration:'+faceDecoration(face)); + if(opts.fontSize!=null)parts.push('font-size:'+opts.fontSize+'em'); + const bx=boxCss(face.box,opts.boxBg); + if(bx)parts.push('box-shadow:'+bx); + return parts.join(';'); +} + +// Single source of truth for the per-face attribute model. One row per +// attribute drives both normalizePkgFace (defaulting + palette resolution) and +// packagesForExport (which attrs serialize and when). Adding a face attribute +// is one row here, not an edit in four hand-kept lists. +// def : value when unset +// resolve : fg/bg/distant-fg run through the palette name->hex resolver +// coerce : 'bool' -> !!v ; 'height' -> v||1 ; default -> v ?? def +// emit : export rule -- 'always' | 'truthy' | 'non-one' | 'bool' +// A hoisted function rather than a const: the inlined page calls normalizePkgFace +// at top level (seedPkgmap) before this point in source order, and a const would +// be in its temporal dead zone there; a function declaration is hoisted. +function faceAttrs(){return [ + {k:'fg', def:null, resolve:true, emit:'always'}, + {k:'bg', def:null, resolve:true, emit:'always'}, + {k:'distant-fg', def:null, resolve:true, emit:'truthy'}, + {k:'family', def:null, emit:'truthy'}, + {k:'weight', def:null, emit:'truthy'}, + {k:'slant', def:null, emit:'truthy'}, + {k:'underline', def:null, emit:'truthy'}, + {k:'strike', def:null, emit:'truthy'}, + {k:'overline', def:null, emit:'truthy'}, + {k:'inherit', def:null, emit:'always'}, + {k:'height', def:1, coerce:'height', emit:'non-one'}, + {k:'box', def:null, emit:'truthy'}, + {k:'inverse', def:false, coerce:'bool', emit:'bool'}, + {k:'extend', def:false, coerce:'bool', emit:'bool'}, +];} + function normalizePkgFace(d,source,palette){ d=migrateLegacyFace(d||{}); const resolve=(v)=>palette?nameToHex(v,palette):v; - return {fg:resolve(d.fg)??null,bg:resolve(d.bg)??null,'distant-fg':resolve(d['distant-fg'])??null,family:d.family??null,weight:d.weight??null,slant:d.slant??null,underline:d.underline??null,strike:d.strike??null,overline:d.overline??null,inherit:d.inherit??null,height:d.height||1,box:d.box??null,inverse:!!d.inverse,extend:!!d.extend,source:source||d.source||'user'}; + const out={}; + for(const a of faceAttrs()){ + let v=a.resolve?resolve(d[a.k]):d[a.k]; + out[a.k]=a.coerce==='bool'?!!v:a.coerce==='height'?(v||1):(v??a.def); + } + out.source=source||d.source||'user'; + return out; } @@ -39,7 +109,8 @@ function normalizePkgFace(d,source,palette){ function buildPkgmap(apps,palette){const m={};for(const app in apps){m[app]={};for(const row of apps[app].faces){m[app][row[0]]=normalizePkgFace(row[2],'default',palette);}}return m;} // The package faces worth exporting (anything seeded or user-touched), trimmed. -function packagesForExport(map){const out={};for(const app in map){const faces={};for(const face in map[app]){const f=map[app][face];if(f.source==='default'||f.source==='user'||f.source==='cleared'){const o={fg:f.fg,bg:f.bg,inherit:f.inherit,source:f.source};if(f.weight)o.weight=f.weight;if(f.slant)o.slant=f.slant;if(f.underline)o.underline=f.underline;if(f.strike)o.strike=f.strike;if(f['distant-fg'])o['distant-fg']=f['distant-fg'];if(f.family)o.family=f.family;if(f.overline)o.overline=f.overline;if(f.inverse)o.inverse=true;if(f.extend)o.extend=true;if(f.height&&f.height!==1)o.height=f.height;if(f.box)o.box=f.box;faces[face]=o;}}if(Object.keys(faces).length)out[app]=faces;}return out;} +// Driven by FACE_ATTRS: each attribute's `emit` rule decides whether it lands. +function packagesForExport(map){const out={};for(const app in map){const faces={};for(const face in map[app]){const f=map[app][face];if(f.source==='default'||f.source==='user'||f.source==='cleared'){const o={};for(const a of faceAttrs()){const v=f[a.k];if(a.emit==='always')o[a.k]=v;else if(a.emit==='truthy'){if(v)o[a.k]=v;}else if(a.emit==='non-one'){if(v&&v!==1)o[a.k]=v;}else if(a.emit==='bool'){if(v)o[a.k]=true;}}o.source=f.source;faces[face]=o;}}if(Object.keys(faces).length)out[app]=faces;}return out;} // Merge an imported package block into a face map, filling missing fields. function mergePackagesInto(map,pkgs){if(!pkgs)return;for(const app in pkgs){if(!map[app])map[app]={};for(const face in pkgs[app]){const f=pkgs[app][face]||{};map[app][face]=normalizePkgFace(f,f.source||'user');}}} @@ -61,16 +132,16 @@ const SYNTAX_INHERIT={cmd:'cm',doc:'str',prop:'var',fnc:'fnd'}; // theme's default foreground (the chain's floor). `dec` (decorator) is pinned to // `ty`: Emacs has no decorator face and renders decorators with // font-lock-type-face, so a dec color set in the studio would never reach Emacs. +// Walk an inherit chain from START, returning the first truthy valueFn(key) or +// null. nextFn(key) gives the parent key; a seen-set guards against a cycle. +function walkInheritChain(start,nextFn,valueFn){ + let k=start;const seen={}; + while(k&&!seen[k]){seen[k]=1;const v=valueFn(k);if(v)return v;k=nextFn(k);} + return null; +} function resolveSyntaxFg(cat,syntax,defaultFg){ - let k=(cat==='dec')?'ty':cat; - const seen={}; - while(k&&!seen[k]){ - seen[k]=1; - const fg=syntax[k]&&syntax[k].fg; - if(fg)return fg; - k=SYNTAX_INHERIT[k]; - } - return defaultFg; + const start=(cat==='dec')?'ty':cat; + return walkInheritChain(start,k=>SYNTAX_INHERIT[k],k=>syntax[k]&&syntax[k].fg)||defaultFg; } // Emacs built-in inherit chains for the ui faces whose parent is also a studio ui @@ -82,15 +153,7 @@ const UI_INHERIT={'mode-line-inactive':'mode-line','line-number-current-line':'l // nothing up the chain is set. The caller applies its own floor (default fg, // ground, or transparent), since that floor differs per attribute and face. function resolveUiAttr(face,attr,uimap){ - let f=face; - const seen={}; - while(f&&!seen[f]){ - seen[f]=1; - const v=uimap[f]&&uimap[f][attr]; - if(v)return v; - f=UI_INHERIT[f]; - } - return null; + return walkInheritChain(face,f=>UI_INHERIT[f],f=>uimap[f]&&uimap[f][attr]); } // Text color for a swatch-dropdown popup row. A row showing a real palette color @@ -169,9 +232,7 @@ function lMax(hue,chroma,fgSet,target){ // the editable truth; these pure functions group it, regenerate a ramp, and plan // assignment re-point across a regenerate. -function oklchOf(hex){return oklab2oklch(srgb2oklab(hex));} function isReservedGroundLikeName(name){return /^(bg|fg)(?:[-_+].+|\d.*)$/i.test(name||'');} -function isPureEndpointHex(hex){const h=(hex||'').toLowerCase();return h==='#ffffff'||h==='#000000';} function interpOklabHex(a,b,t,offset){ const lab={L:a.L+(b.L-a.L)*t,a:a.a+(b.a-a.a)*t,b:a.b+(b.b-a.b)*t}; const lrgb=oklab2lrgb(lab.L,lab.a,lab.b); @@ -476,4 +537,4 @@ function overflowNonDefault(cur,def,showInheritHeight){ return false; } -export { nameToHex, migrateLegacyFace, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, galleryModel, appViewKeysSorted, faceBoxNonDefaults, overflowNonDefault, stepViewIndex, spanNeighborHex, slugify, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, usedPaletteHexes, paletteUsages, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet }; +export { nameToHex, migrateLegacyFace, cssWeight, faceDecoration, boxCss, faceCss, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, galleryModel, appViewKeysSorted, faceBoxNonDefaults, overflowNonDefault, stepViewIndex, spanNeighborHex, slugify, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, usedPaletteHexes, paletteUsages, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet }; |
