feat(desktop): add complete theme switching and suppress mouse selection

This commit is contained in:
Kinneyzhang 2026-09-11 02:07:28 +08:00
parent b94e19ad7a
commit af9803f8aa
8 changed files with 441 additions and 76 deletions

View File

@ -24,10 +24,12 @@ For native help, pointer, hover-style, or keymap demonstration work, maintain `e
For layer, overlap, positioning, or anchored-menu demonstrations, maintain `examples/layer-reference.ebox` and its same-basename companion, preserve `examples/layer-minimal.ebox` as the minimal fixture, and run `make layer-tests`. Extend `tests/ebox-playground-layer-tests.el` for actual native commands and committed-snapshot render parity instead of creating another demo runner or probe. Keep layout in the DSL, business state/actions in the companion, and one font size throughout positioned content. Use the shared interaction evaluator above for repeated layer-command timing. Each parent creates its local stacking group automatically; use `:layer root` on a nested absolute node when root projection is required, with semantic `:anchor` and `:placement` declarations for anchored menus.
For direct-manipulation desktop windows, click-to-front, and title-bar dragging,
For direct-manipulation desktop windows, click-to-front, title-bar dragging,
complete light/dark palettes, and desktop-only mouse selection policy,
maintain `examples/desktop-reference.ebox` with its companion and run
`make desktop-tests`. Reuse `tests/ebox-playground-desktop-tests.el` for native
press/motion/release streams, intermediate publication and fresh-render parity.
press/motion/release streams, gap/end-of-buffer selection fallbacks, palette
round trips, intermediate publication and fresh-render parity.
Use the shared interaction evaluator with the desktop's `"01 ST"` dock label
and `"M-<right>"` for repeated position publication timing. Keep native gesture
state in the companion, use public Ebox updates, and preserve the existing layer

View File

@ -94,6 +94,12 @@
cyan Signal, amber Notes, mint Terminal and rose Palette accents. One configured
font size throughout. Use contrasting title bars and compact pane content,
rather than fake close controls, images or additional font packages.
- Light theme: a warm pale shell, blue-gray workspace, and five pastel card
bodies with dark ink. Retune every caption, active/inactive title, dock entry,
waveform, artwork and swatch; changing only the workspace background is not
an acceptable light theme. Keep text contrast strong and decorative dots quiet.
Palette declarations stay in the DSL; semantic classes connect them to the
companion's public selector/region updates, without a global stylesheet.
- Structure: compact identity/utility bar, clipped desktop with five independent
sibling windows, then persistent dock and status. Keep the workspace dominant;
each pane contains a distinct editorial/code/task/signal composition.
@ -101,8 +107,15 @@
during native mouse tracking, apply the final release position, and retain the
last accepted position after interruption. No inertial or decorative animation.
The current Ebox layer contract uses pixel X and host-text-row Y coordinates.
- Mouse selection: gaps, background rows, line endings and buffer end are desktop
surfaces too. A desktop-only minor mode remaps native selection fallbacks;
control keymaps keep their click/drag behavior. Remove the policy on unmount
or replacement by another example. Do not change global mouse bindings.
- Keyboard: RET/SPC activates a pane or dock entry; title-bar Alt+arrows move it.
Reset restores the original arrangement. No changes to global Emacs bindings.
- LIGHT/DARK toggles the complete palette in one publication and keeps geometry,
stacking and active window. Reset keeps the chosen theme; fresh previews start
dark and keep independent theme state.
- Responsive behavior: use viewport-relative positions and bounded pane widths;
overflow is clipped at the desktop boundary. Dock entries remain available for
covered panes. Target at least 40 columns and 20 text rows; smaller workspaces

View File

@ -13,12 +13,21 @@
Palette。各窗口有独立内容和局部层叠全部使用当前字体大小标题栏与底部窗口栏标出
活动窗口,不添加假的关闭按钮或自动动画。
亮色主题使用暖白外框、浅蓝灰工作区和五种浅色卡片,搭配深色正文。标题栏、窗口入口、
辅助文字、波形、嵌套图案与色板都单独适配,不能只把工作台背景换亮。配色表保留在
DSL 中,以语义 class 连接 companion 的公开 selector/region 更新,不使用全局样式表。
点击露出的标题、正文或底部窗口入口时整窗置顶;按住标题栏拖拽时即时发布位置,保留
鼠标抓取偏移,并应用松手的最终位置。横向按像素,纵向遵循文本行网格。中断保留最后
成功的位置,手势退出不留下 timer、advice 或全局键位。RET/SPC 激活,标题或窗口栏的
Alt+方向键移动窗口RESET 恢复原始位置与次序。业务状态只在 Ebox 发布成功后提交,
每个预览独立。
LIGHT/DARK 在一次发布中切换完整配色保留位置、层次和活动窗口RESET 不改变主题。
新预览默认深色。Desktop 局部 minor mode 拦截背景、按钮间隙、换行和缓冲区末尾的原生
鼠标选区命令,保留按钮与标题栏自身的行为。卸载或切换成其他示例后移除该策略,
不修改 Emacs 的全局鼠标绑定。
结构为紧凑页头与工具栏、裁剪的主工作区、固定窗口栏和坐标状态;推荐至少 40 列、
20 行。较小尺寸保留窗口入口RESET 可找回拖出边界的窗口。测试覆盖真实 keymap
命令、中间运动帧、松手、遮挡归属、失败回滚及独立渲染一致性。遵守不操作用户 GUI

View File

@ -53,6 +53,13 @@ text and layered artwork. All text uses the configured font size.
- The five dock entries raise windows even when they are completely covered.
**CYCLE** brings the backmost window forward; **RESET** restores all initial
positions and the original stack, including panes clipped off the desktop.
- **LIGHT/DARK** switches the complete palette: workspace, all five card bodies,
text, title bars, dock, artwork, swatches and hover colors. Light mode uses
pastel cards with dark ink. Window positions and stacking survive a toggle;
**RESET** keeps the chosen theme.
- Mouse clicks and drags do not select text, including between dock buttons and
at line/buffer ends. This policy belongs only to the Desktop preview; ordinary
buffers and other examples keep their normal mouse behavior.
- `RET`/`SPC` activates the control at point. `Alt+arrows` on a title or dock
entry moves its pane; point follows the same native control for key repeat.
`C-g` stops a drag and keeps the last accepted position.

View File

@ -43,6 +43,13 @@ ECSS 0.1.0 与 TP 1.0.1 是互相独立的包,安装顺序任意;两者都
控件,标题或窗口入口上的 Alt+方向键移动对应窗口并支持连续按键C-g 结束拖拽,
保留最后成功发布的位置。每次打开的预览状态互相独立。
**LIGHT/DARK** 切换整套配色:五张卡片在亮色模式下都使用浅色底与深色文字,标题栏、
窗口入口、装饰图案、波形、色板和悬停颜色也同步适配。切换保留窗口位置与层次,
**RESET** 保留所选主题。每个新预览默认深色。
Desktop 中的鼠标点击与拖动不会选中文本,按钮之间、换行和缓冲区末尾也受保护。
这项策略只作用于 Desktop 预览,其他缓冲区和示例仍保留正常的鼠标行为。
横向按像素移动,纵向遵循 Ebox 的文本行网格。推荐至少 40 列、20 行,更大的预览
有更多摆放空间。所有文本保持当前字体大小,不使用额外字体或图片依赖。

View File

@ -1,164 +1,246 @@
;; Layout and initial geometry live here; native gestures live in the companion.
(let* ((studio (desktop-demo-window-create
(let* ((styles
'((desktop-root (:background-color "#0B1020" :color "#CFD8EA")
(:background-color "#F3F0E8" :color "#263347"))
(desktop-brand (:color "#BBA9FF") (:color "#674795"))
(desktop-heading (:color "#F1F5FF") (:color "#18263B"))
(desktop-muted (:color "#8F9FBD") (:color "#53637A"))
(desktop-quiet (:color "#657899") (:color "#596A84"))
(desktop-control (:background-color "#25324B" :color "#D7E3F8"
:hover-style (:background-color "#354662"))
(:background-color "#CBD6E4" :color "#253247"
:hover-style (:background-color "#B9C9DE")))
(desktop-chrome (:background-color "#25324B" :color "#C1CBE0")
(:background-color "#D5DDE8" :color "#344359"))
(desktop-workspace (:background-color "#111A2C")
(:background-color "#E3E8EF"))
(desktop-grid-label (:color "#3F5273") (:color "#566B86"))
(desktop-grid-dots (:color "#334866") (:color "#A6B3C3"))
(desktop-grid-hint (:color "#5A6D8E") (:color "#566A83"))
(desktop-studio (:background-color "#26223B" :color "#D6CDEF")
(:background-color "#E9E3F2" :color "#4F3F67"))
(desktop-studio-heading (:color "#F5F0FF") (:color "#37284F"))
(desktop-studio-caption (:color "#A999C8") (:color "#67577E"))
(desktop-studio-text (:color "#B8A9D0") (:color "#5F4D78"))
(desktop-studio-muted (:color "#85799F") (:color "#6D587F"))
(desktop-art-form (:background-color "#7561AC" :color "#EAE1FF")
(:background-color "#C9BCE3" :color "#453259"))
(desktop-art-space (:background-color "#BBA9FF" :color "#302449")
(:background-color "#DED3F6" :color "#51356C"))
(desktop-art-play (:background-color "#D8EAAB" :color "#303D22")
(:background-color "#DBE8C3" :color "#41512B"))
(desktop-notes (:background-color "#EEE3CB" :color "#514430")
(:background-color "#FFF4D8" :color "#5B482B"))
(desktop-notes-caption (:color "#8C744E") (:color "#7A623E"))
(desktop-notes-muted (:color "#887651") (:color "#836D49"))
(desktop-notes-rule (:color "#A18B61") (:color "#A38E65"))
(desktop-notes-footer (:color "#746345") (:color "#705935"))
(desktop-signal (:background-color "#17343E" :color "#BFE5E9")
(:background-color "#DBEEF0" :color "#315B65"))
(desktop-signal-heading (:color "#E4FBF9") (:color "#184B57"))
(desktop-signal-caption (:color "#7BAEB9") (:color "#436971"))
(desktop-signal-muted (:color "#699CA9") (:color "#496F77"))
(desktop-signal-accent (:color "#8DE1E4") (:color "#2E7280"))
(desktop-wave-muted (:background-color "#518F9D") (:background-color "#9EC9CF"))
(desktop-wave-cyan (:background-color "#8DE1E4") (:background-color "#5EADB5"))
(desktop-wave-violet (:background-color "#BBA9FF") (:background-color "#927AC9"))
(desktop-terminal (:background-color "#101F22" :color "#B6D7CC")
(:background-color "#E0EEE4" :color "#355E4C"))
(desktop-terminal-accent (:color "#A7E8BB") (:color "#256544"))
(desktop-terminal-muted (:color "#608C7F") (:color "#496D5B"))
(desktop-palette (:background-color "#39283D" :color "#EBCBDE")
(:background-color "#F0E1EB" :color "#65485E"))
(desktop-palette-caption (:color "#AF8DA9") (:color "#7B5670"))
(desktop-palette-accent (:color "#F3A8C9") (:color "#925277"))
(desktop-swatch-violet (:background-color "#BBA9FF") (:background-color "#9578CF"))
(desktop-swatch-amber (:background-color "#F6C879") (:background-color "#DEB251"))
(desktop-swatch-cyan (:background-color "#8DE1E4") (:background-color "#58A7AF"))
(desktop-swatch-mint (:background-color "#A7E8BB") (:background-color "#72AB83"))
(desktop-swatch-rose (:background-color "#F3A8C9") (:background-color "#CE87AA"))))
(studio (desktop-demo-window-create
:id "desk-studio" :label "STUDIO" :accent "#BBA9FF" :ink "#241B40"
:light-accent "#6D4AA2" :light-ink "#FFFFFF"
:left '(% 5) :top '(lh 1)))
(notes (desktop-demo-window-create
:id "desk-notes" :label "NOTES" :accent "#F6C879" :ink "#3B2917"
:light-accent "#8C6421" :light-ink "#FFFFFF"
:left '(% 45) :top '(% 4)))
(signal (desktop-demo-window-create
:id "desk-signal" :label "SIGNAL" :accent "#8DE1E4" :ink "#15383D"
:light-accent "#246675" :light-ink "#FFFFFF"
:left '(% 57) :top '(% 32)))
(terminal (desktop-demo-window-create
:id "desk-terminal" :label "TERMINAL" :accent "#A7E8BB" :ink "#1B3826"
:light-accent "#2F674B" :light-ink "#FFFFFF"
:left '(% 8) :top '(% 46)))
(palette (desktop-demo-window-create
:id "desk-palette" :label "PALETTE" :accent "#F3A8C9" :ink "#402438"
:light-accent "#875270" :light-ink "#FFFFFF"
:left '(% 43) :top '(% 65)))
(panes (list studio notes signal terminal palette))
(demo (desktop-demo-create :windows panes
(demo (desktop-demo-create :windows panes :styles styles
:order (mapcar #'desktop-demo-window-id panes)
:active "desk-palette")))
`(column :width (vw 100) :height (vh 100) :overflow hidden
:background-color "#0B1020" :color "#CFD8EA"
`(column :id "desktop-root" :class desktop-root
:width (vw 100) :height (vh 100) :overflow hidden
,@(desktop-demo-style demo 'desktop-root)
(box :height (lh 3) :padding-inline (ch 1)
(row :height (lh 1)
(box :color "#BBA9FF" :font-weight bold "P A R A L L A X")
(box :padding-left (ch 2) :color "#657899" "DESKTOP / 05"))
(box :color "#F1F5FF" "A little space. A lot of possibilities.")
(box :color "#8F9FBD" "Click to focus. Drag a title bar to move."))
(box :class desktop-brand ,@(desktop-demo-style demo 'desktop-brand)
:font-weight bold "P A R A L L A X")
(box :padding-left (ch 2) :class desktop-quiet
,@(desktop-demo-style demo 'desktop-quiet) "DESKTOP / 05"))
(box :class desktop-heading ,@(desktop-demo-style demo 'desktop-heading)
"A little space. A lot of possibilities.")
(box :class desktop-muted ,@(desktop-demo-style demo 'desktop-muted)
"Click to focus. Drag a title bar to move."))
(flex :height (lh 1) :gap ((lh 0) (ch 1)) :padding-inline (ch 1)
(box :padding-inline (ch 1) :color "#D7E3F8" :background-color "#25324B"
:pointer hand :hover-style (:background-color "#354662")
(box :padding-inline (ch 1) :flex-shrink 0 :class desktop-control
,@(desktop-demo-style demo 'desktop-control) :pointer hand
:help-echo "Restore the five original positions and stacking order."
:keymap ,(desktop-demo-control demo 'reset) "RESET")
(box :padding-inline (ch 1) :color "#D7E3F8" :background-color "#25324B"
:pointer hand :hover-style (:background-color "#354662")
(box :padding-inline (ch 1) :flex-shrink 0 :class desktop-control
,@(desktop-demo-style demo 'desktop-control) :pointer hand
:help-echo "Bring the backmost window to the front."
:keymap ,(desktop-demo-control demo 'next) "CYCLE")
(box :color "#657899" "FIVE IDEAS / ONE SHARED SPACE"))
(box :id "desktop-theme" :class desktop-control
,@(desktop-demo-style demo 'desktop-control)
:width (ch 7) :flex-shrink 0 :text-align center :pointer hand
:help-echo "Switch the whole desktop between light and dark palettes."
:keymap ,(desktop-demo-control demo 'theme) "LIGHT")
(box :class desktop-quiet :min-width (px 0) :overflow hidden :wrap-mode none
,@(desktop-demo-style demo 'desktop-quiet)
"FIVE IDEAS / ONE SHARED SPACE"))
(box :id "desktop-canvas" :height (calc (- (vh 100) (lh 7)))
:overflow hidden :background-color "#111A2C"
:overflow hidden :class desktop-workspace
,@(desktop-demo-style demo 'desktop-workspace)
(box :position absolute :left (ch 1) :top (lh 0)
:color "#3F5273" "WORKSPACE 01 + + + + +")
:class desktop-grid-label ,@(desktop-demo-style demo 'desktop-grid-label)
"WORKSPACE 01 + + + + +")
(box :position absolute :left (% 68) :top (% 78)
:width (% 30) :color "#334866"
:width (% 30) :class desktop-grid-dots ,@(desktop-demo-style demo 'desktop-grid-dots)
"+ . + . +\n. + . + .\n+ . + . +\n. + . + .")
(box :position absolute :left (ch 2) :top (% 93)
:color "#5A6D8E" "MOVE THINGS AROUND. MAKE IT YOURS.")
:class desktop-grid-hint ,@(desktop-demo-style demo 'desktop-grid-hint)
"MOVE THINGS AROUND. MAKE IT YOURS.")
(box :id "desk-studio" :position absolute :left ,(desktop-demo-window-left studio)
:top ,(desktop-demo-window-top studio) :width (min (ch 44) (% 55))
:height (lh 13) :z-index 1 :overflow hidden
:background-color "#26223B" :color "#D6CDEF" :pointer arrow
:class desktop-studio ,@(desktop-demo-style demo 'desktop-studio) :pointer arrow
:help-echo "Studio. Click any exposed area to bring this whole window forward."
:keymap ,(desktop-demo-keymap demo "desk-studio")
(box :id "desk-studio-title" :height (lh 1) :padding-inline (ch 1)
:background-color "#25324B" :color "#C1CBE0" :font-weight bold
:class desktop-chrome ,@(desktop-demo-style demo 'desktop-chrome) :font-weight bold
:pointer hand :keymap ,(desktop-demo-keymap demo "desk-studio" t)
:help-echo "Drag Studio. Alt+arrows moves it from the keyboard."
"STUDIO / 01 ::")
(box :padding ((lh 1) (ch 2))
(box :font-weight bold :color "#F5F0FF" "Make space for the unexpected.")
(box :padding-top (lh 1) :color "#A999C8" "EXPERIMENT 024 / SOFT STRUCTURES")
(box :font-weight bold :class desktop-studio-heading ,@(desktop-demo-style demo 'desktop-studio-heading) "Make space for the unexpected.")
(box :padding-top (lh 1) :class desktop-studio-caption ,@(desktop-demo-style demo 'desktop-studio-caption) "EXPERIMENT 024 / SOFT STRUCTURES")
(box :height (lh 5) :padding-top (lh 1)
(box :position absolute :left (ch 0) :top (lh 1)
:width (ch 13) :height (lh 3) :background-color "#7561AC"
:color "#EAE1FF" :text-align center "\nFORM / 01")
:width (ch 13) :height (lh 3) :class desktop-art-form ,@(desktop-demo-style demo 'desktop-art-form) :text-align center "\nFORM / 01")
(box :position absolute :left (ch 9) :top (lh 0)
:width (ch 13) :height (lh 3) :z-index 1
:background-color "#BBA9FF" :color "#302449"
:class desktop-art-space ,@(desktop-demo-style demo 'desktop-art-space)
:text-align center "\nSPACE / 02")
(box :position absolute :left (ch 18) :top (lh 2)
:width (ch 12) :height (lh 3) :z-index 2
:background-color "#D8EAAB" :color "#303D22"
:class desktop-art-play ,@(desktop-demo-style demo 'desktop-art-play)
:text-align center "\nPLAY / 03"))
(box :color "#B8A9D0" "Fragments become a composition.")
(box :padding-top (lh 1) :color "#85799F" "COLLECTION / UNFINISHED POSSIBILITIES")))
(box :class desktop-studio-text ,@(desktop-demo-style demo 'desktop-studio-text) "Fragments become a composition.")
(box :padding-top (lh 1) :class desktop-studio-muted ,@(desktop-demo-style demo 'desktop-studio-muted) "COLLECTION / UNFINISHED POSSIBILITIES")))
(box :id "desk-notes" :position absolute :left ,(desktop-demo-window-left notes)
:top ,(desktop-demo-window-top notes) :width (min (ch 34) (% 43))
:height (lh 11) :z-index 2 :overflow hidden
:background-color "#EEE3CB" :color "#514430" :pointer arrow
:class desktop-notes ,@(desktop-demo-style demo 'desktop-notes) :pointer arrow
:help-echo "Notes. The content remains here even when another window covers it."
:keymap ,(desktop-demo-keymap demo "desk-notes")
(box :id "desk-notes-title" :height (lh 1) :padding-inline (ch 1)
:background-color "#25324B" :color "#C1CBE0" :font-weight bold
:class desktop-chrome ,@(desktop-demo-style demo 'desktop-chrome) :font-weight bold
:pointer hand :keymap ,(desktop-demo-keymap demo "desk-notes" t)
:help-echo "Drag Notes. Alt+arrows moves it from the keyboard."
"NOTES / 02 ::")
(box :padding ((lh 1) (ch 1))
(box :font-weight bold "A quieter kind of focus.")
(box :padding-top (lh 1) :color "#8C744E" "TODAY / THINGS WORTH MAKING")
(box :padding-top (lh 1) :class desktop-notes-caption ,@(desktop-demo-style demo 'desktop-notes-caption) "TODAY / THINGS WORTH MAKING")
(box :padding-top (lh 1)
"[x] Collect the small ideas\n[ ] Give the good ones room\n[ ] Leave something unfinished")
(box :padding-top (lh 1) :color "#887651" "Less noise. More intention.")
(box :color "#A18B61" "------------------------------")
(box :color "#746345" "03 NOTES / SAVED IN THE MOMENT")))
(box :padding-top (lh 1) :class desktop-notes-muted ,@(desktop-demo-style demo 'desktop-notes-muted) "Less noise. More intention.")
(box :class desktop-notes-rule ,@(desktop-demo-style demo 'desktop-notes-rule) "------------------------------")
(box :class desktop-notes-footer ,@(desktop-demo-style demo 'desktop-notes-footer) "03 NOTES / SAVED IN THE MOMENT")))
(box :id "desk-signal" :position absolute :left ,(desktop-demo-window-left signal)
:top ,(desktop-demo-window-top signal) :width (min (ch 32) (% 39))
:height (lh 10) :z-index 3 :overflow hidden
:background-color "#17343E" :color "#BFE5E9" :pointer arrow
:class desktop-signal ,@(desktop-demo-style demo 'desktop-signal) :pointer arrow
:help-echo "Signal. Raise the entire player, including its overlapping waveform."
:keymap ,(desktop-demo-keymap demo "desk-signal")
(box :id "desk-signal-title" :height (lh 1) :padding-inline (ch 1)
:background-color "#25324B" :color "#C1CBE0" :font-weight bold
:class desktop-chrome ,@(desktop-demo-style demo 'desktop-chrome) :font-weight bold
:pointer hand :keymap ,(desktop-demo-keymap demo "desk-signal" t)
:help-echo "Drag Signal. Alt+arrows moves it from the keyboard."
"SIGNAL / 03 ::")
(box :padding ((lh 1) (ch 2))
(box :font-weight bold :color "#E4FBF9" "AFTER HOURS")
(box :color "#7BAEB9" "A soundtrack for the in-between.")
(box :font-weight bold :class desktop-signal-heading ,@(desktop-demo-style demo 'desktop-signal-heading) "AFTER HOURS")
(box :class desktop-signal-caption ,@(desktop-demo-style demo 'desktop-signal-caption) "A soundtrack for the in-between.")
(flex :height (lh 3) :gap ((lh 0) (ch 1)) :padding-top (lh 1)
(box :width (ch 2) :height (lh 1) :background-color "#518F9D")
(box :width (ch 2) :height (lh 2) :background-color "#8DE1E4")
(box :width (ch 2) :height (lh 1) :background-color "#518F9D")
(box :width (ch 2) :height (lh 2) :background-color "#BBA9FF")
(box :width (ch 2) :height (lh 1) :background-color "#8DE1E4")
(box :width (ch 2) :height (lh 2) :background-color "#BBA9FF")
(box :width (ch 2) :height (lh 1) :background-color "#518F9D"))
(box :color "#8DE1E4" "========------------ 03:42")
(box :padding-top (lh 1) :color "#699CA9" "SIDE B / SLOWER ORBITS")))
(box :width (ch 2) :height (lh 1) :class desktop-wave-muted ,@(desktop-demo-style demo 'desktop-wave-muted))
(box :width (ch 2) :height (lh 2) :class desktop-wave-cyan ,@(desktop-demo-style demo 'desktop-wave-cyan))
(box :width (ch 2) :height (lh 1) :class desktop-wave-muted ,@(desktop-demo-style demo 'desktop-wave-muted))
(box :width (ch 2) :height (lh 2) :class desktop-wave-violet ,@(desktop-demo-style demo 'desktop-wave-violet))
(box :width (ch 2) :height (lh 1) :class desktop-wave-cyan ,@(desktop-demo-style demo 'desktop-wave-cyan))
(box :width (ch 2) :height (lh 2) :class desktop-wave-violet ,@(desktop-demo-style demo 'desktop-wave-violet))
(box :width (ch 2) :height (lh 1) :class desktop-wave-muted ,@(desktop-demo-style demo 'desktop-wave-muted)))
(box :class desktop-signal-accent ,@(desktop-demo-style demo 'desktop-signal-accent) "========------------ 03:42")
(box :padding-top (lh 1) :class desktop-signal-muted ,@(desktop-demo-style demo 'desktop-signal-muted) "SIDE B / SLOWER ORBITS")))
(box :id "desk-terminal" :position absolute :left ,(desktop-demo-window-left terminal)
:top ,(desktop-demo-window-top terminal) :width (min (ch 48) (% 60))
:height (lh 11) :z-index 4 :overflow hidden
:background-color "#101F22" :color "#B6D7CC" :pointer arrow
:class desktop-terminal ,@(desktop-demo-style demo 'desktop-terminal) :pointer arrow
:help-echo "Terminal. This sample transcript is part of the movable text window."
:keymap ,(desktop-demo-keymap demo "desk-terminal")
(box :id "desk-terminal-title" :height (lh 1) :padding-inline (ch 1)
:background-color "#25324B" :color "#C1CBE0" :font-weight bold
:class desktop-chrome ,@(desktop-demo-style demo 'desktop-chrome) :font-weight bold
:pointer hand :keymap ,(desktop-demo-keymap demo "desk-terminal" t)
:help-echo "Drag Terminal. Alt+arrows moves it from the keyboard."
"TERMINAL / 04 ::")
(box :padding ((lh 1) (ch 2))
(box :color "#A7E8BB" "$ build something good")
(box :padding-top (lh 1) :color "#608C7F" " session: creative / local\n workspace: parallax")
(box :class desktop-terminal-accent ,@(desktop-demo-style demo 'desktop-terminal-accent) "$ build something good")
(box :padding-top (lh 1) :class desktop-terminal-muted ,@(desktop-demo-style demo 'desktop-terminal-muted) " session: creative / local\n workspace: parallax")
(box :padding-top (lh 1) " + ideas connected\n + room for experiments\n + all five windows ready")
(box :padding-top (lh 1) :color "#A7E8BB" " READY / follow your curiosity")))
(box :padding-top (lh 1) :class desktop-terminal-accent ,@(desktop-demo-style demo 'desktop-terminal-accent) " READY / follow your curiosity")))
(box :id "desk-palette" :position absolute :left ,(desktop-demo-window-left palette)
:top ,(desktop-demo-window-top palette) :width (min (ch 29) (% 36))
:height (lh 9) :z-index 5 :overflow hidden
:background-color "#39283D" :color "#EBCBDE" :pointer arrow
:class desktop-palette ,@(desktop-demo-style demo 'desktop-palette) :pointer arrow
:help-echo "Palette starts in front. Click a window underneath to change the stack."
:keymap ,(desktop-demo-keymap demo "desk-palette")
(box :id "desk-palette-title" :height (lh 1) :padding-inline (ch 1)
:background-color "#F3A8C9" :color "#402438" :font-weight bold
:class desktop-chrome ,@(desktop-demo-chrome-style demo palette t) :font-weight bold
:pointer hand :keymap ,(desktop-demo-keymap demo "desk-palette" t)
:help-echo "Drag Palette. Alt+arrows moves it from the keyboard."
"PALETTE / 05 ::")
(box :padding ((lh 1) (ch 1))
(box :font-weight bold "Objects in conversation.")
(box :padding-top (lh 1) :color "#AF8DA9" "FIVE TONES / ONE ATMOSPHERE")
(box :padding-top (lh 1) :class desktop-palette-caption ,@(desktop-demo-style demo 'desktop-palette-caption) "FIVE TONES / ONE ATMOSPHERE")
(flex :height (lh 2) :padding-top (lh 1) :gap ((lh 0) (ch 1))
(box :width (ch 4) :height (lh 1) :background-color "#BBA9FF")
(box :width (ch 4) :height (lh 1) :background-color "#F6C879")
(box :width (ch 4) :height (lh 1) :background-color "#8DE1E4")
(box :width (ch 4) :height (lh 1) :background-color "#A7E8BB")
(box :width (ch 4) :height (lh 1) :background-color "#F3A8C9"))
(box :padding-top (lh 1) :color "#F3A8C9" "MIDNIGHT / SOFT ELECTRIC"))))
(box :width (ch 4) :height (lh 1) :class desktop-swatch-violet
,@(desktop-demo-style demo 'desktop-swatch-violet))
(box :width (ch 4) :height (lh 1) :class desktop-swatch-amber
,@(desktop-demo-style demo 'desktop-swatch-amber))
(box :width (ch 4) :height (lh 1) :class desktop-swatch-cyan
,@(desktop-demo-style demo 'desktop-swatch-cyan))
(box :width (ch 4) :height (lh 1) :class desktop-swatch-mint
,@(desktop-demo-style demo 'desktop-swatch-mint))
(box :width (ch 4) :height (lh 1) :class desktop-swatch-rose
,@(desktop-demo-style demo 'desktop-swatch-rose)))
(box :padding-top (lh 1) :class desktop-palette-accent ,@(desktop-demo-style demo 'desktop-palette-accent) "MIDNIGHT / SOFT ELECTRIC"))))
(flex :height (lh 2) :gap ((lh 0) (ch 1)) :padding-top (lh 1)
,@(cl-loop for pane in panes for index from 1
@ -167,12 +249,11 @@
collect
`(box :id ,(concat (desktop-demo-window-id pane) "-dock")
:padding-inline (ch 1)
:background-color ,(if active (desktop-demo-window-accent pane) "#25324B")
:color ,(if active (desktop-demo-window-ink pane) "#C1CBE0")
:class desktop-chrome ,@(desktop-demo-chrome-style demo pane active)
:font-weight bold :pointer hand
:help-echo ,(format "Bring %s to front, even when covered. Alt+arrows moves it."
(desktop-demo-window-label pane))
:keymap ,(desktop-demo-keymap demo (desktop-demo-window-id pane))
,(format "%02d %s" index short))))
(box :id "desktop-status" :height (lh 1) :color "#8F9FBD"
(box :id "desktop-status" :height (lh 1) :class desktop-muted ,@(desktop-demo-style demo 'desktop-muted)
,(desktop-demo-status demo))))

View File

@ -9,14 +9,72 @@
(require 'cl-lib)
(require 'ebox)
(require 'ebox-selector)
(defun desktop-demo--dismiss-mouse-selection ()
"Consume a native text-selection command in a desktop preview."
(interactive)
(deactivate-mark))
(defvar desktop-demo-input-mode-map
(let ((map (make-sparse-keymap)))
(dolist (command '(mouse-drag-region mouse-set-region mouse-set-point
mouse-save-then-kill mouse-drag-secondary
mouse-start-secondary mouse-set-secondary
mouse-secondary-save-then-kill))
(define-key map (vector 'remap command) #'desktop-demo--dismiss-mouse-selection))
map)
"Desktop-local fallbacks for mouse events outside individual controls.")
(defun desktop-demo--clear-mouse-mark ()
"Clear a preceding text selection when a desktop mouse command starts."
(when (mouse-event-p last-input-event)
(deactivate-mark)))
(define-minor-mode desktop-demo-input-mode
"Keep desktop gestures from entering native text selection.
Control text keymaps retain their own activation and title-drag commands."
:lighter nil :keymap desktop-demo-input-mode-map
(if desktop-demo-input-mode
(progn
(deactivate-mark)
(add-hook 'pre-command-hook #'desktop-demo--clear-mouse-mark nil t))
(remove-hook 'pre-command-hook #'desktop-demo--clear-mouse-mark t)))
(defun desktop-demo--sync-input-mode ()
"Apply the desktop mouse policy only to a successfully mounted desktop."
(desktop-demo-input-mode
(if (and ebox-buffer-mode
(condition-case nil
(ebox-region-resolve (current-buffer) "desktop-root")
(user-error nil)))
1 -1)))
(add-hook 'ebox-buffer-mode-hook #'desktop-demo--sync-input-mode)
(cl-defstruct (desktop-demo-window (:constructor desktop-demo-window-create))
"A desktop pane's identity, palette, initial offsets and pixel displacement."
id label accent ink left top (x 0) (y 0))
id label accent ink light-accent light-ink left top (x 0) (y 0))
(cl-defstruct (desktop-demo (:constructor desktop-demo-create))
"Independent state captured by the native commands of one desktop preview."
windows order active (serial 5) dragging)
windows order active styles (theme 'dark) (serial 5) dragging)
(defun desktop-demo-style (demo role)
"Return DEMO's current theme declarations for the semantic style ROLE."
(let ((entry (assq role (desktop-demo-styles demo))))
(unless entry (error "Unknown desktop style: %s" role))
(nth (if (eq (desktop-demo-theme demo) 'light) 2 1) entry)))
(defun desktop-demo-chrome-style (demo pane active)
"Return DEMO's themed title/dock declarations for PANE and ACTIVE state."
(if active
(if (eq (desktop-demo-theme demo) 'light)
(list :background-color (desktop-demo-window-light-accent pane)
:color (desktop-demo-window-light-ink pane))
(list :background-color (desktop-demo-window-accent pane)
:color (desktop-demo-window-ink pane)))
(desktop-demo-style demo 'desktop-chrome)))
(defun desktop-demo--window (demo id)
"Return DEMO's pane with semantic ID."
@ -31,13 +89,11 @@
(desktop-demo-window-label pane)
(desktop-demo-window-x pane) (desktop-demo-window-y pane))))
(defun desktop-demo--chrome (pane active)
"Return title and dock updates for PANE, highlighted when ACTIVE."
(let ((background (if active (desktop-demo-window-accent pane) "#25324B"))
(foreground (if active (desktop-demo-window-ink pane) "#C1CBE0")))
(defun desktop-demo--chrome (demo pane active)
"Return DEMO's themed title and dock updates for PANE and ACTIVE state."
(let ((style (desktop-demo-chrome-style demo pane active)))
(mapcar (lambda (suffix)
(list (concat (desktop-demo-window-id pane) suffix)
:background-color background :color foreground))
(cons (concat (desktop-demo-window-id pane) suffix) style))
'("-title" "-dock"))))
(defun desktop-demo--keyboard-anchor ()
@ -75,6 +131,7 @@ Failed Ebox publication leaves the business state on its accepted generation."
(setf (desktop-demo-windows demo) (desktop-demo-windows candidate)
(desktop-demo-order demo) (desktop-demo-order candidate)
(desktop-demo-active demo) (desktop-demo-active candidate)
(desktop-demo-theme demo) (desktop-demo-theme candidate)
(desktop-demo-serial demo) (desktop-demo-serial candidate)
(desktop-demo-dragging demo) (desktop-demo-dragging candidate))
(desktop-demo--restore-keyboard-anchor anchor)))
@ -92,8 +149,8 @@ Failed Ebox publication leaves the business state on its accepted generation."
demo candidate
(append (list (list id :z-index (desktop-demo-serial candidate)))
(desktop-demo--chrome
(desktop-demo--window demo (desktop-demo-active demo)) nil)
(desktop-demo--chrome pane t)))))
demo (desktop-demo--window demo (desktop-demo-active demo)) nil)
(desktop-demo--chrome demo pane t)))))
(when dragging (setf (desktop-demo-dragging demo) id))))
(defun desktop-demo-move (demo id x y &optional row-height)
@ -223,11 +280,29 @@ or failure releases gesture state and keeps the last accepted geometry."
:top (desktop-demo-window-top pane) :z-index depth) updates)
(setq updates
(append updates (desktop-demo--chrome
pane (equal active (desktop-demo-window-id pane))))))
demo pane (equal active (desktop-demo-window-id pane))))))
(setf (desktop-demo-windows candidate) panes (desktop-demo-order candidate) ids
(desktop-demo-active candidate) active (desktop-demo-serial candidate) (length ids))
(desktop-demo--publish demo candidate updates)))
(defun desktop-demo-toggle-theme (demo)
"Switch DEMO's complete palette without changing window geometry or order."
(let ((candidate (copy-desktop-demo demo)) updates)
(setf (desktop-demo-theme candidate)
(if (eq (desktop-demo-theme demo) 'dark) 'light 'dark))
(dolist (entry (desktop-demo-styles demo))
(dolist (match (ebox-selector-query-buffer
(current-buffer) (concat "." (symbol-name (car entry)))))
(when-let* ((handle (plist-get match :region-handle)))
(push (cons handle (desktop-demo-style candidate (car entry))) updates))))
(desktop-demo--publish
demo candidate
(append (nreverse updates)
(desktop-demo--chrome
candidate (desktop-demo--window demo (desktop-demo-active demo)) t)
(list (list "desktop-theme" :content
(if (eq (desktop-demo-theme candidate) 'light) "DARK" "LIGHT")))))))
(defun desktop-demo-control (demo action)
"Create a native toolbar command for DEMO's ACTION."
(ebox-keymap-create
@ -236,6 +311,7 @@ or failure releases gesture state and keeps the last accepted geometry."
(pcase action
('reset (desktop-demo-reset demo))
('next (desktop-demo-activate demo (car (desktop-demo-order demo))))
('theme (desktop-demo-toggle-theme demo))
(_ (error "Unknown desktop action: %s" action))))))
(provide 'desktop-demo-reference)

View File

@ -65,6 +65,7 @@
(list (copy-sequence (desktop-demo-order demo))
(desktop-demo-active demo) (desktop-demo-serial demo)
(desktop-demo-dragging demo)
(desktop-demo-theme demo)
(mapcar (lambda (window)
(list (desktop-demo-window-id window)
(desktop-demo-window-x window)
@ -134,6 +135,175 @@ TYPE defaults to `down-mouse-1'; repeated presses use their native event type."
(should (equal (desktop-demo-active ebox-playground-desktop-test--demo)
(cdr entry))))))
(ert-deftest ebox-playground-desktop-gaps-and-endpoints-do-not-select-text ()
"Desktop gaps, newlines and buffer end consume native selection commands."
(ebox-playground-desktop-test--with-preview (buffer)
(should desktop-demo-input-mode)
(let* ((dock (ebox-playground-layer-test--position "01 ST"))
(gap (next-single-property-change dock 'keymap nil (point-max)))
(newline (save-excursion (goto-char dock) (line-end-position)))
(transient-mark-mode t))
(save-window-excursion
(set-window-buffer (selected-window) buffer)
(dolist (position (list (point-min) gap newline (point-max)))
(dolist (type '(down-mouse-1 mouse-1 drag-mouse-1 S-mouse-1
M-down-mouse-1 M-mouse-1 M-drag-mouse-1))
(goto-char position)
(set-mark (if (= position (point-min)) (point-max) (point-min)))
(setq mark-active t)
(let* ((event (ebox-playground-desktop-test--event
type (selected-window) position '(0 . 0)))
(command (key-binding (vector type)))
(last-input-event event))
(should (eq command #'desktop-demo--dismiss-mouse-selection))
(call-interactively command)
(should-not mark-active))))))))
(ert-deftest ebox-playground-desktop-fast-dock-clicks-remain-controls ()
"Single, double and triple dock click sequences never activate a region."
(ebox-playground-desktop-test--with-preview (buffer)
(let ((transient-mark-mode t))
(save-window-excursion
(set-window-buffer (selected-window) buffer)
(dolist (label '("01 ST" "02 NT" "03 SG" "04 TM" "05 PL"))
(dolist (type '(down-mouse-1 mouse-1 double-down-mouse-1 double-mouse-1
triple-down-mouse-1 triple-mouse-1 drag-mouse-1
double-drag-mouse-1 triple-drag-mouse-1))
(let* ((position (ebox-playground-layer-test--position label))
(event (ebox-playground-desktop-test--event
type (selected-window) position '(0 . 0)))
(last-input-event event))
(goto-char position)
(set-mark (point-min))
(setq mark-active t)
(run-hooks 'pre-command-hook)
(let ((command (key-binding (vector type))))
(should (commandp command))
(should-not (memq command '(mouse-drag-region mouse-set-region mouse-set-point)))
(call-interactively command))
(should-not mark-active))))))
(ebox-playground-layer-test--parity)))
(ert-deftest ebox-playground-desktop-mouse-policy-is-local-and-released ()
"Other buffers and replacement previews retain ordinary Emacs selection."
(ebox-playground-desktop-test--with-preview (buffer)
(with-temp-buffer
(should-not desktop-demo-input-mode)
(should (eq (key-binding [down-mouse-1]) 'mouse-drag-region)))
(ebox-playground-open-file ebox-playground-layer-test--file (buffer-name buffer))
(should-not desktop-demo-input-mode)
(should-not (memq #'desktop-demo--clear-mouse-mark pre-command-hook))
(ebox-playground-open-file ebox-playground-desktop-test--file (buffer-name buffer))
(should desktop-demo-input-mode)
(ebox-unmount-buffer buffer)
(should-not desktop-demo-input-mode)))
(ert-deftest ebox-playground-desktop-theme-adapts-all-card-and-chrome-styles ()
"A theme toggle changes every semantic paint role without moving windows."
(ebox-playground-desktop-test--with-preview (buffer)
(let* ((demo ebox-playground-desktop-test--demo)
(before (ebox-playground-desktop-test--state))
(revision (ebox-playground-desktop-test--revision)))
(ebox-playground-layer-test--activate "LIGHT")
(should (eq (desktop-demo-theme demo) 'light))
(should (= (ebox-playground-desktop-test--revision) (1+ revision)))
(dolist (entry (desktop-demo-styles demo))
(let ((matches (ebox-selector-query-buffer
buffer (concat "." (symbol-name (car entry))))))
(should matches)
(unless (eq (car entry) 'desktop-chrome)
(dolist (match matches)
(let ((node (plist-get match :node)) (style (nth 2 entry)))
(while style
(let ((property (pop style)) (expected (pop style)))
(unless (eq property :hover-style)
(should (equal (ebox-get node (if (eq property :background-color)
:bgcolor property))
expected))))))))))
(should (string-match-p "DARK" (buffer-string)))
(should (member "#B9C9DE"
(flatten-tree (get-text-property
(ebox-playground-layer-test--position "DARK") 'mouse-face))))
(ebox-playground-layer-test--activate "DARK")
(should (equal before (ebox-playground-desktop-test--state)))
(should (eq (desktop-demo-theme demo) 'dark)))))
(ert-deftest ebox-playground-desktop-light-theme-keeps-focus-move-and-reset ()
"Light cards and active chrome stay light after normal window operations."
(ebox-playground-desktop-test--with-preview (buffer 40 20)
(ebox-playground-layer-test--activate "LIGHT")
(ebox-playground-layer-test--activate "01 ST")
(let* ((demo ebox-playground-desktop-test--demo)
(position (ebox-playground-layer-test--position "01 ST"))
(face (flatten-tree (get-text-property position 'face))))
(should (member "#6D4AA2" face))
(should (member "#FFFFFF" face))
(ebox-playground-layer-test--activate "01 ST" (kbd "M-<right>"))
(ebox-playground-layer-test--activate "RESET")
(should (eq (desktop-demo-theme demo) 'light))
(should (equal (desktop-demo-active demo) "desk-palette"))
(should (member "#875270" (flatten-tree
(get-text-property
(ebox-playground-layer-test--position "05 PL") 'face))))
(dolist (label '("01 ST" "02 NT" "03 SG" "04 TM" "05 PL" "DARK"))
(should (ebox-playground-layer-test--position label))))))
(ert-deftest ebox-playground-desktop-failed-theme-keeps-palette-and-state ()
"Rejected theme publication rolls back the complete palette and state."
(ebox-playground-desktop-test--with-preview (buffer)
(dolist (failure-step '(text client-state))
(let ((before (buffer-string))
(state (ebox-playground-desktop-test--state))
(tp--surface-publication-step-function
(lambda (step _surface)
(when (eq step failure-step) (error "Reject theme")))))
(should-error (ebox-playground-layer-test--activate "LIGHT"))
(should (equal state (ebox-playground-desktop-test--state)))
(should (equal-including-properties before (buffer-string)))))
(ebox-playground-layer-test--activate "LIGHT")))
(ert-deftest ebox-playground-desktop-light-palette-has-pale-cards-and-dark-ink ()
"Every light card is pale and its body/caption text has useful contrast."
(ebox-playground-desktop-test--with-preview (buffer)
(let ((styles (desktop-demo-styles ebox-playground-desktop-test--demo)))
(cl-labels
((light (role) (nth 2 (assq role styles)))
(luminance (hex)
(cl-loop for start in '(1 3 5) for weight in '(0.2126 0.7152 0.0722)
for value = (/ (string-to-number (substring hex start (+ start 2)) 16) 255.0)
sum (* weight (if (<= value 0.04045) (/ value 12.92)
(expt (/ (+ value 0.055) 1.055) 2.4)))))
(contrast (fg bg)
(let ((a (luminance fg)) (b (luminance bg)))
(/ (+ (max a b) 0.05) (+ (min a b) 0.05)))))
(dolist (role '(desktop-root desktop-workspace desktop-studio desktop-notes
desktop-signal desktop-terminal desktop-palette))
(let ((style (light role)))
(should (> (luminance (plist-get style :background-color)) 0.7))
(when (plist-get style :color)
(should (> (contrast (plist-get style :color)
(plist-get style :background-color)) 4.5)))))
(dolist (pair '((desktop-studio-muted . desktop-studio)
(desktop-notes-caption . desktop-notes)
(desktop-signal-caption . desktop-signal)
(desktop-terminal-muted . desktop-terminal)
(desktop-palette-caption . desktop-palette)))
(should (> (contrast (plist-get (light (car pair)) :color)
(plist-get (light (cdr pair)) :background-color)) 4.5)))))))
(ert-deftest ebox-playground-desktop-previews-have-independent-themes ()
"Changing one desktop palette cannot recolor a second preview."
(ebox-playground-desktop-test--with-preview (first)
(let ((first-demo ebox-playground-desktop-test--demo))
(ebox-playground-desktop-test--with-preview (second)
(let ((before (buffer-string)))
(with-current-buffer first
(desktop-demo-toggle-theme first-demo)
(ebox-playground-layer-test--parity))
(should (equal-including-properties before (buffer-string)))
(should (eq (desktop-demo-theme ebox-playground-desktop-test--demo) 'dark))
(should (eq (desktop-demo-theme first-demo) 'light)))))))
(ert-deftest ebox-playground-desktop-repeated-keyboard-nudges-retain-dock-binding ()
"Repeated arrow commands keep their native dock binding without repositioning."
(dolist (key '("M-<right>" "M-<left>" "M-<up>" "M-<down>"))