From 298e24aa8a0fdd88d2ae8ecb514b3e18b2b4bb5b Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Fri, 3 Jul 2026 00:02:48 -0400 Subject: feat(theme-studio): pin the remaining chrome at absolute heights tab-bar, tab-line, line-number, and line-number-current-line join mode-line in the chrome height seeds (apply_modeline_height_default generalized to apply_chrome_height_defaults), each pinned at absolute 130 so no bar or gutter tracks a buffer's enlarged default face. header-line and mode-line-inactive stay unseeded on purpose: both inherit mode-line, so the pin reaches them through the chain and their own value would duplicate state. The line-number pair is seeded individually because the generated theme's explicit specs leave their :inherit unspecified at runtime. header-line, tab-bar, and tab-line also join the UI faces table, so all chrome heights are editable through the size column; the mock-completeness gate exempts the three faces the mock deliberately doesn't draw. WIP.json reconciled and the theme regenerated; every chrome face resolves :height 130 in the live daemon, pins and inherit chains both. --- scripts/theme-studio/WIP.json | 40 +++++++++++++++++++++++++++++++-- scripts/theme-studio/browser-gates.js | 5 ++++- scripts/theme-studio/generate.py | 41 ++++++++++++++++++++++------------ scripts/theme-studio/test_generate.py | 38 ++++++++++++++++++------------- scripts/theme-studio/theme-studio.html | 9 +++++--- themes/WIP-theme.el | 6 +++-- 6 files changed, 101 insertions(+), 38 deletions(-) diff --git a/scripts/theme-studio/WIP.json b/scripts/theme-studio/WIP.json index b839190a..22abeb7f 100644 --- a/scripts/theme-studio/WIP.json +++ b/scripts/theme-studio/WIP.json @@ -1012,7 +1012,8 @@ "inverse": false, "extend": false, "inherit": null, - "height": null + "height": 130, + "heightMode": "abs" }, "line-number-current-line": { "fg": "#e6ce88", @@ -1028,7 +1029,8 @@ "inverse": false, "extend": false, "inherit": null, - "height": null + "height": 130, + "heightMode": "abs" }, "minibuffer-prompt": { "fg": "#899bb1", @@ -1208,6 +1210,40 @@ "extend": false, "inherit": null, "height": null + }, + "tab-bar": { + "fg": null, + "bg": null, + "distant-fg": null, + "family": null, + "weight": null, + "slant": null, + "underline": null, + "strike": null, + "overline": null, + "box": null, + "inverse": false, + "extend": false, + "inherit": null, + "height": 130, + "heightMode": "abs" + }, + "tab-line": { + "fg": null, + "bg": null, + "distant-fg": null, + "family": null, + "weight": null, + "slant": null, + "underline": null, + "strike": null, + "overline": null, + "box": null, + "inverse": false, + "extend": false, + "inherit": null, + "height": 130, + "heightMode": "abs" } }, "locks": [ diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index a6b2b4be..3ccec8ea 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -187,7 +187,10 @@ if(location.hash==='#mocktest')gate('mocktest',A=>withSavedState(['UIMAP','PKGMA UIMAP['link']={fg:'#112233',bg:'#aabbcc',weight:null,slant:null,underline:{style:'line',color:null},strike:null,box:null};buildMockFrame(); const linkStyled=Q('[data-face="link"]'),linkSt=linkStyled&&linkStyled.getAttribute('style')||''; A(linkSt.includes('#112233')&&linkSt.includes('#aabbcc'),'inline UI face preview honors fg and bg: '+linkSt); - const missing=UI_FACES.map(f=>f[0]).filter(f=>!Q('[data-face="'+f+'"]')); + // header-line/tab-bar/tab-line are deliberately not in the mock (editable- + // height spec, Decision 4: their row sample suffices), so the sweep skips them + const MOCK_EXEMPT=['header-line','tab-bar','tab-line']; + const missing=UI_FACES.map(f=>f[0]).filter(f=>!MOCK_EXEMPT.includes(f)&&!Q('[data-face="'+f+'"]')); A(missing.length===0,'all UI faces are represented in live buffer preview: '+missing.join(',')); buildTable();buildUITable();buildPkgTable(); [['#legbody tr[data-kind="kw"]',5],['#uibody tr[data-face="mode-line"]',5],['#pkgbody tr',5]].forEach(([sel,idx])=>{ diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py index 0d061805..b0fafefd 100644 --- a/scripts/theme-studio/generate.py +++ b/scripts/theme-studio/generate.py @@ -218,19 +218,29 @@ def apply_builtin_fallback_styles(uimap): for face in ("mode-line","mode-line-inactive"): uimap[face]["box"]={"style":"released","width":1,"color":None} -def apply_modeline_height_default(uimap): - """Seed an absolute height on mode-line so it never tracks the buffer default. - - mode-line's :height is unspecified in stock Emacs, so it inherits the - buffer's default face height -- a buffer that remaps default larger (the - nov-reading view) inflates its modeline with it. A fixed 1/10pt integer - pins the bar. 130 matches the configured laptop default-height; editable - once the height control ships (theme-studio-editable-height-spec). - mode-line-inactive inherits mode-line, so it gets no seed of its own.""" - face = uimap.get("mode-line") - if face and face.get("height") is None: - face["height"] = 130 - face["heightMode"] = "abs" +# The chrome faces that carry their own absolute height pin. header-line and +# mode-line-inactive are deliberately absent: both inherit mode-line, so the +# pin reaches them through the chain and a seed of their own would duplicate +# state. The line-number pair is seeded individually because the generated +# theme's explicit specs leave their :inherit unspecified at runtime. +CHROME_HEIGHT_SEEDS = ("mode-line", "tab-bar", "tab-line", + "line-number", "line-number-current-line") + + +def apply_chrome_height_defaults(uimap): + """Seed absolute heights on the chrome so it never tracks the buffer default. + + Chrome :height is unspecified in stock Emacs, so it follows the buffer's + default face height -- a buffer that remaps default larger (the + nov-reading view) inflates its modeline and gutters with it. A fixed + 1/10pt integer pins each bar. 130 matches the configured laptop + default-height; every seed is editable in the studio's size column + (theme-studio-editable-height-spec).""" + for name in CHROME_HEIGHT_SEEDS: + face = uimap.get(name) + if face and face.get("height") is None: + face["height"] = 130 + face["heightMode"] = "abs" def apply_hover_box_default(uimap): """Seed the mode-line hover face's box. @@ -252,7 +262,7 @@ def build_uimap(ui_faces,defaults): uimap={face[0]:ui_face_spec() for face in ui_faces} apply_builtin_fallback_styles(uimap) apply_hover_box_default(uimap) - apply_modeline_height_default(uimap) + apply_chrome_height_defaults(uimap) return uimap def build_syntax(cols,map_,bold,italic,defaults): @@ -338,6 +348,9 @@ UI_FACES=[["cursor","cursor","Aa|"],["region","region (selection)","selected tex ["mode-line","mode-line","status active"], ["mode-line-highlight","mode-line-highlight (hover)","git:main"], ["mode-line-inactive","mode-line-inactive","status idle"], + ["header-line","header-line","breadcrumb / doc info"], + ["tab-bar","tab-bar","tabs"], + ["tab-line","tab-line","buffer tabs"], ["fringe","fringe","| |"],["line-number","line-number"," 42"], ["line-number-current-line","line-number-current-line","> 42"],["minibuffer-prompt","minibuffer-prompt","M-x "], ["isearch","isearch (match)","match"],["lazy-highlight","lazy-highlight","other match"], diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py index 28c9b88c..0415d04f 100644 --- a/scripts/theme-studio/test_generate.py +++ b/scripts/theme-studio/test_generate.py @@ -328,29 +328,35 @@ class GeneratorStateHelpers(unittest.TestCase): generate.apply_hover_box_default(uimap) self.assertEqual(uimap["mode-line-highlight"]["box"], {"style": "line", "width": 2, "color": "#abcdef"}) - def test_mode_line_defaults_to_absolute_height(self): - # mode-line must carry a fixed 1/10pt height so it never tracks a + def test_chrome_faces_default_to_absolute_height(self): + # The chrome faces carry a fixed 1/10pt height so they never track a # buffer's enlarged default face (the nov-reading modeline bug). # Both branches: with and without a defaults snapshot. - self.assertEqual(generate.UIMAP["mode-line"]["height"], 130) - self.assertIsInstance(generate.UIMAP["mode-line"]["height"], int) no_snapshot = generate.build_uimap(generate.UI_FACES, DefaultFaces(None)) - self.assertEqual(no_snapshot["mode-line"]["height"], 130) - - def test_mode_line_inactive_gets_no_height_seed(self): - # mode-line-inactive inherits mode-line's absolute height; seeding its - # own value would just duplicate state. + for face in ("mode-line", "tab-bar", "tab-line", + "line-number", "line-number-current-line"): + self.assertEqual(generate.UIMAP[face]["height"], 130, face) + self.assertIsInstance(generate.UIMAP[face]["height"], int, face) + self.assertEqual(generate.UIMAP[face]["heightMode"], "abs", face) + self.assertEqual(no_snapshot[face]["height"], 130, face) + + def test_inheriting_chrome_gets_no_height_seed(self): + # mode-line-inactive and header-line inherit mode-line's absolute + # height; seeding their own values would just duplicate state. self.assertIsNone(generate.UIMAP["mode-line-inactive"]["height"]) + self.assertIsNone(generate.UIMAP["header-line"]["height"]) - def test_modeline_height_default_yields_to_existing_height(self): - uimap = {"mode-line": ui_face_spec({"height": 142})} - generate.apply_modeline_height_default(uimap) + def test_chrome_height_default_yields_to_existing_height(self): + uimap = {"mode-line": ui_face_spec({"height": 142}), + "tab-bar": ui_face_spec()} + generate.apply_chrome_height_defaults(uimap) self.assertEqual(uimap["mode-line"]["height"], 142) + self.assertEqual(uimap["tab-bar"]["height"], 130) - def test_modeline_height_seed_carries_abs_kind(self): - # the seed is a fixed 1/10pt pin, so its kind is explicit -- never - # left for number-type inference (JSON can't carry the distinction) - self.assertEqual(generate.UIMAP["mode-line"]["heightMode"], "abs") + def test_ui_faces_include_the_full_chrome_set(self): + names = [row[0] for row in generate.UI_FACES] + for face in ("header-line", "tab-bar", "tab-line"): + self.assertIn(face, names) def test_migrate_legacy_infers_height_kind(self): # mirrors app-core.js migrateLegacyFace: integer -> abs, fractional diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 2f74bee9..cede7e1e 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -302,10 +302,10 @@