From 0890cda100295cbfef7a3f0ad43d0e3c784965cf Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 18 Jun 2026 19:55:33 -0500 Subject: feat(theme-studio): add mode-line-highlight as an editable face The mode-line hover box (the raised bevel on clickable mode-line segments) came from mode-line-highlight, a face the studio never managed, so it fell through to Emacs's stock released-button default with no way to change it. I added it to the generated UI face list, between mode-line and mode-line-inactive. The row and box control are already generic over that list, so they appear automatically. build-theme.el's UI emission is generic too, so the elisp side needs nothing. The face isn't in the captured Emacs snapshot, so apply_hover_box_default seeds its box to the raised default in both build_uimap branches. That matches current behavior and leaves the user free to flatten or recolor it. The mock frame previews the hover by wrapping a mode-line segment in the face. --- scripts/theme-studio/app.js | 5 +++-- scripts/theme-studio/generate.py | 25 +++++++++++++++++++++---- scripts/theme-studio/test_generate.py | 13 +++++++++++++ scripts/theme-studio/theme-studio.html | 9 +++++---- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index fea46f227..3949268d0 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -439,7 +439,7 @@ function syncMockHeight(){const t=document.getElementById('uitable'),m=document. function buildMockFrame(){ const fr=document.getElementById('mockframe');if(!fr)return; const bg=MAP['bg'],fg=MAP['p']; - const ln=uf('line-number'),lnc=uf('line-number-current-line'),hl=uf('hl-line'),hil=uf('highlight'),reg=uf('region'),isr=uf('isearch'),isf=uf('isearch-fail'),laz=uf('lazy-highlight'),par=uf('show-paren-match'),parx=uf('show-paren-mismatch'),cur=uf('cursor'),ml=uf('mode-line'),mli=uf('mode-line-inactive'),mb=uf('minibuffer-prompt'),frng=uf('fringe'),vb=uf('vertical-border'),lnk=uf('link'),err=uf('error'),wrn=uf('warning'),suc=uf('success'); + const ln=uf('line-number'),lnc=uf('line-number-current-line'),hl=uf('hl-line'),hil=uf('highlight'),reg=uf('region'),isr=uf('isearch'),isf=uf('isearch-fail'),laz=uf('lazy-highlight'),par=uf('show-paren-match'),parx=uf('show-paren-mismatch'),cur=uf('cursor'),ml=uf('mode-line'),mli=uf('mode-line-inactive'),mlh=uf('mode-line-highlight'),mb=uf('minibuffer-prompt'),frng=uf('fringe'),vb=uf('vertical-border'),lnk=uf('link'),err=uf('error'),wrn=uf('warning'),suc=uf('success'); const lines=[ {t:[['cmd',';; '],['cm','init.el - your config']]}, {t:[['punc','('],['kw','require'],['p',' '],['con',"'cl-lib"],['punc',')']]}, @@ -500,7 +500,8 @@ function buildMockFrame(){ buf+=`
${L.cont?'↪':''}${i+1}${cd||' '}
`; }); let html=`
${buf}
`; - html+=`
init.el (Emacs Lisp) L5 git:main
`; + const mlhStyle=uiCss(mlh,mlh.fg||ml.fg||bg,mlh.bg||ml.bg||fg); + html+=`
init.el (Emacs Lisp) L5 git:main
`; html+=`
*Messages* (Fundamental)
`; html+=`
I-search: count zzz [no match]
`; html+=`
https://gnu.org error warning ok
`; diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py index c489b79cc..ae31afae2 100644 --- a/scripts/theme-studio/generate.py +++ b/scripts/theme-studio/generate.py @@ -108,11 +108,26 @@ 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_hover_box_default(uimap): + """Seed the mode-line hover face's box. + + `mode-line-highlight` (applied via mouse-face to the clickable mode-line + segments) is absent from the captured Emacs snapshot, so seed() returns + blank for it in both branches below. Emacs's stock default is a raised + released-button box; default to that so the studio reflects current + behavior, then let the user flatten or recolor it. A future snapshot that + captures the face wins (the box-already-set guard leaves it alone).""" + face=uimap.get("mode-line-highlight") + if face and not face.get("box"): + face["box"]={"style":"released","width":1,"color":None} + def build_uimap(ui_faces,defaults): if defaults.available: - return {face[0]:ui_face_spec(defaults.seed(face[0],False)) for face in ui_faces} - uimap={face[0]:ui_face_spec() for face in ui_faces} - apply_builtin_fallback_styles(uimap) + uimap={face[0]:ui_face_spec(defaults.seed(face[0],False)) for face in ui_faces} + else: + uimap={face[0]:ui_face_spec() for face in ui_faces} + apply_builtin_fallback_styles(uimap) + apply_hover_box_default(uimap) return uimap def build_syntax(cols,map_,bold,italic,defaults): @@ -197,7 +212,9 @@ CATS=[["bg","bg (ground)","Aa Bb 123"],["p","fg","other / whitespace"],["kw","ke ["punc","punctuation","{ } ( ) ;"]] UI_FACES=[["cursor","cursor","Aa|"],["region","region (selection)","selected text"], ["hl-line","hl-line (current line)","current line"],["highlight","highlight","hover"], - ["mode-line","mode-line","status active"],["mode-line-inactive","mode-line-inactive","status idle"], + ["mode-line","mode-line","status active"], + ["mode-line-highlight","mode-line-highlight (mode-line hover)","git:main"], + ["mode-line-inactive","mode-line-inactive","status idle"], ["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 ed2ce74ba..a0ca91e06 100644 --- a/scripts/theme-studio/test_generate.py +++ b/scripts/theme-studio/test_generate.py @@ -240,6 +240,19 @@ class GeneratorStateHelpers(unittest.TestCase): self.assertTrue(uimap["link"]["underline"]) self.assertEqual(uimap["mode-line"]["box"], {"style": "released", "width": 1, "color": None}) + def test_mode_line_highlight_defaults_to_raised_box(self): + # The face is absent from the snapshot, so it must get the raised box in + # both the with-snapshot and no-snapshot branches. + raised = {"style": "released", "width": 1, "color": None} + self.assertEqual(generate.UIMAP["mode-line-highlight"]["box"], raised) + no_snapshot = generate.build_uimap(generate.UI_FACES, DefaultFaces(None)) + self.assertEqual(no_snapshot["mode-line-highlight"]["box"], raised) + + def test_hover_box_default_yields_to_existing_box(self): + uimap = {"mode-line-highlight": ui_face_spec({"box": {"style": "line", "width": 2, "color": "#abcdef"}})} + generate.apply_hover_box_default(uimap) + self.assertEqual(uimap["mode-line-highlight"]["box"], {"style": "line", "width": 2, "color": "#abcdef"}) + def test_build_syntax_uses_map_and_style_fallbacks_without_defaults_snapshot(self): syntax = generate.build_syntax( {"kw": [None, True]}, diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 8b8a5f75c..f0bbd178f 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -270,9 +270,9 @@