fix: keep workbench card heights independent when toggling help
This commit is contained in:
parent
920efc36f7
commit
ffafd668ab
19
DESIGN.md
19
DESIGN.md
@ -3,7 +3,7 @@
|
|||||||
## Source of truth
|
## Source of truth
|
||||||
|
|
||||||
- Status: Active
|
- Status: Active
|
||||||
- Last refreshed: 2026-09-05
|
- Last refreshed: 2026-09-08
|
||||||
- Primary product surface: generic `etaf-playground.el` workspace plus the
|
- Primary product surface: generic `etaf-playground.el` workspace plus the
|
||||||
`examples/research-shelf.etaf` / `.el` / `.ecss` consumer triplet
|
`examples/research-shelf.etaf` / `.el` / `.ecss` consumer triplet
|
||||||
- Additional product surface: `examples/task-workbench.el`, a complete
|
- Additional product surface: `examples/task-workbench.el`, a complete
|
||||||
@ -253,15 +253,22 @@ lane passed.
|
|||||||
- Responsive: retain declarative Flex wrapping in source order. Side columns
|
- Responsive: retain declarative Flex wrapping in source order. Side columns
|
||||||
have stable basis sizes; the task list grows. Root padding and viewport scroll
|
have stable basis sizes; the task list grows. Root padding and viewport scroll
|
||||||
are legitimate composition and must work together without example workarounds.
|
are legitimate composition and must work together without example workarounds.
|
||||||
- Panel alignment: each workspace Flex line stretches its columns to a shared
|
- Panel alignment: workspace panels align at the top and keep their own
|
||||||
height, keeping the filter panel's bottom aligned with the main content. A
|
content-driven heights in layout lines. Toggling “显示说明” collapses the
|
||||||
wrapped row sizes independently; no viewport-specific height constants.
|
detail content without changing the filter, task or service line counts. Following cards
|
||||||
|
and the footer may move naturally in document flow; do not reserve invisible
|
||||||
|
help space or introduce viewport-specific height constants. This follows
|
||||||
|
the user's 2026-09-08 review of disclosure-induced panel resizing.
|
||||||
|
- Native text geometry: Emacs shares ascent and descent across each displayed
|
||||||
|
row. Moving content may therefore change a card's pixel height while its
|
||||||
|
layout line count stays fixed. The user explicitly accepts this behavior;
|
||||||
|
preserve layout semantics and interaction instead of forcing uniform rows.
|
||||||
- Width distribution: filter/list/inspector grow with weights `1/4/1`, keeping
|
- Width distribution: filter/list/inspector grow with weights `1/4/1`, keeping
|
||||||
emphasis on the task list while a side column fills its line when wrapped.
|
emphasis on the task list while a side column fills its line when wrapped.
|
||||||
Filter buttons wrap into a horizontal group when their panel has room.
|
Filter buttons wrap into a horizontal group when their panel has room.
|
||||||
Detail and service cards share a wrapping Flex: stacked in the desktop side
|
Detail and service cards share a wrapping Flex: stacked in the desktop side
|
||||||
column, side by side with equal heights on a wider wrapped row, then stacked
|
column, side by side with independent heights on a wider wrapped row, then
|
||||||
again when each card's usable minimum width no longer fits.
|
stacked again when each card's usable minimum width no longer fits.
|
||||||
- Table sizing: the task title receives the remaining column width through the
|
- Table sizing: the task title receives the remaining column width through the
|
||||||
existing `(fr 1)` Grid track convention; the action column keeps its compact
|
existing `(fr 1)` Grid track convention; the action column keeps its compact
|
||||||
character width. Header and body consume the same tracks. Resizing changes
|
character width. Header and body consume the same tracks. Resizing changes
|
||||||
|
|||||||
@ -129,7 +129,7 @@
|
|||||||
:align-items 'center
|
:align-items 'center
|
||||||
(slot :name 'toolbar)))
|
(slot :name 'toolbar)))
|
||||||
(flex :width 'stretch :flex-wrap 'wrap :gap '(1 (16))
|
(flex :width 'stretch :flex-wrap 'wrap :gap '(1 (16))
|
||||||
:align-items 'stretch
|
:align-items 'flex-start
|
||||||
(slot))
|
(slot))
|
||||||
(slot :name 'footer (text "准备就绪")))
|
(slot :name 'footer (text "准备就绪")))
|
||||||
:styles
|
:styles
|
||||||
@ -338,7 +338,7 @@
|
|||||||
(etaf-data-load data))))
|
(etaf-data-load data))))
|
||||||
|
|
||||||
(flex :flex-wrap 'wrap :flex-basis 36 :flex-grow 1 :min-width 0
|
(flex :flex-wrap 'wrap :flex-basis 36 :flex-grow 1 :min-width 0
|
||||||
:align-content 'space-between :align-items 'stretch :gap '(1 (16))
|
:align-content 'space-between :align-items 'flex-start :gap '(1 (16))
|
||||||
(wb-detail :flex-basis 28 :flex-grow 1)
|
(wb-detail :flex-basis 28 :flex-grow 1)
|
||||||
(wb-service :flex-basis 28 :flex-grow 1))
|
(wb-service :flex-basis 28 :flex-grow 1))
|
||||||
|
|
||||||
|
|||||||
@ -195,6 +195,49 @@ cannot establish that Emacs painted a control on the same visual line."
|
|||||||
(list identity (car bounds) (cdr bounds))))
|
(list identity (car bounds) (cdr bounds))))
|
||||||
(number-sequence 1 10)))))
|
(number-sequence 1 10)))))
|
||||||
|
|
||||||
|
(defun wb-gui--panel-heights (context)
|
||||||
|
"Observe CONTEXT's four card identities and layout lines in source order."
|
||||||
|
(let ((runtime (wb-gui--runtime context))
|
||||||
|
panels)
|
||||||
|
(with-current-buffer (etaf-gui-verifier-context-target-buffer context)
|
||||||
|
(dolist (entry (etaf-runtime-host-props-entries runtime))
|
||||||
|
(let ((class (plist-get (cdr entry) :class)))
|
||||||
|
(when (member "etaf-panel" (if (stringp class) (split-string class) class))
|
||||||
|
(let* ((bounds (etaf-host-ref-bounds runtime (car entry)))
|
||||||
|
(start (car bounds))
|
||||||
|
(end (and bounds (1- (cdr bounds)))))
|
||||||
|
(unless (and start end)
|
||||||
|
(error "Workbench panel has no live bounds"))
|
||||||
|
(push (list :ref (car entry) :start start
|
||||||
|
:lines (1+ (- (line-number-at-pos end)
|
||||||
|
(line-number-at-pos start))))
|
||||||
|
panels))))))
|
||||||
|
(unless (= (length panels) 4)
|
||||||
|
(error "Expected four Workbench panels, got %d" (length panels)))
|
||||||
|
(sort panels (lambda (left right)
|
||||||
|
(< (plist-get left :start) (plist-get right :start))))))
|
||||||
|
|
||||||
|
(defun wb-gui--help-layout-preserved-p (context expanded-p)
|
||||||
|
"Check CONTEXT's disclosure state and unaffected card heights in layout lines.
|
||||||
|
EXPANDED-P also requires the detail card to recover its original height.
|
||||||
|
Native Emacs glyph rows may have different pixel heights as content moves."
|
||||||
|
(let* ((before (etaf-gui-verifier-context-get context 'help-panels-before))
|
||||||
|
(after (wb-gui--panel-heights context))
|
||||||
|
(height (lambda (panels index)
|
||||||
|
(plist-get (nth index panels) :lines))))
|
||||||
|
(etaf-gui-verifier-context-put context 'help-panels-current after)
|
||||||
|
(and before
|
||||||
|
(eq expanded-p (and (wb-gui--text-p context "点击行:选择任务") t))
|
||||||
|
(equal (mapcar (lambda (panel) (plist-get panel :ref)) before)
|
||||||
|
(mapcar (lambda (panel) (plist-get panel :ref)) after))
|
||||||
|
(cl-every (lambda (index)
|
||||||
|
(= (funcall height before index)
|
||||||
|
(funcall height after index)))
|
||||||
|
'(0 1 3))
|
||||||
|
(if expanded-p
|
||||||
|
(= (funcall height before 2) (funcall height after 2))
|
||||||
|
(< (funcall height after 2) (funcall height before 2))))))
|
||||||
|
|
||||||
(defun wb-gui--capture-theme-baseline (context)
|
(defun wb-gui--capture-theme-baseline (context)
|
||||||
"Capture CONTEXT's initial geometry once it survives another paint turn."
|
"Capture CONTEXT's initial geometry once it survives another paint turn."
|
||||||
(when (etaf-gui-verifier-context-get context 'light-layout)
|
(when (etaf-gui-verifier-context-get context 'light-layout)
|
||||||
@ -507,10 +550,15 @@ cannot establish that Emacs painted a control on the same visual line."
|
|||||||
(lambda (c) (wb-gui--enter c "每页条数 10 ✎" "2"))
|
(lambda (c) (wb-gui--enter c "每页条数 10 ✎" "2"))
|
||||||
(lambda (c) (and (equal (wb-gui--ids c) '(2 3))
|
(lambda (c) (and (equal (wb-gui--ids c) '(2 3))
|
||||||
(wb-gui--text-p c "Page 1 / 50"))))
|
(wb-gui--text-p c "Page 1 / 50"))))
|
||||||
(wb-gui--action "hide-help" (lambda (c) (wb-gui--press c "显示说明"))
|
(wb-gui--action
|
||||||
(lambda (c) (not (wb-gui--text-p c "点击行:选择任务"))))
|
"hide-help"
|
||||||
|
(lambda (c)
|
||||||
|
(etaf-gui-verifier-context-put
|
||||||
|
c 'help-panels-before (wb-gui--panel-heights c))
|
||||||
|
(wb-gui--press c "显示说明"))
|
||||||
|
(lambda (c) (wb-gui--help-layout-preserved-p c nil)))
|
||||||
(wb-gui--action "show-help" (lambda (c) (wb-gui--press c "显示说明"))
|
(wb-gui--action "show-help" (lambda (c) (wb-gui--press c "显示说明"))
|
||||||
(lambda (c) (wb-gui--text-p c "点击行:选择任务")))
|
(lambda (c) (wb-gui--help-layout-preserved-p c t)))
|
||||||
(wb-gui--action "resource-failure" (lambda (c) (wb-gui--press c "模拟失败"))
|
(wb-gui--action "resource-failure" (lambda (c) (wb-gui--press c "模拟失败"))
|
||||||
(lambda (c) (wb-gui--text-p c "模拟加载失败")))
|
(lambda (c) (wb-gui--text-p c "模拟加载失败")))
|
||||||
(wb-gui--action "resource-retry" (lambda (c) (wb-gui--press c "刷新 / 重试"))
|
(wb-gui--action "resource-retry" (lambda (c) (wb-gui--press c "刷新 / 重试"))
|
||||||
|
|||||||
@ -108,15 +108,50 @@
|
|||||||
(line-number-at-pos
|
(line-number-at-pos
|
||||||
(etaf-host-ref-position runtime (wb-test--control runtime "新增任务")))))))
|
(etaf-host-ref-position runtime (wb-test--control runtime "新增任务")))))))
|
||||||
|
|
||||||
(cl-defmacro wb-test--with-app ((runtime data) &rest body)
|
(defun wb-test--card-layout (runtime control-label)
|
||||||
"Mount a fresh app bound as RUNTIME and DATA, then execute BODY."
|
"Return public bounds and line geometry for CONTROL-LABEL's card in RUNTIME."
|
||||||
|
(let* ((control (wb-test--control runtime control-label))
|
||||||
|
(panels
|
||||||
|
(cl-remove-if-not
|
||||||
|
(lambda (entry)
|
||||||
|
(let ((classes (plist-get (cdr entry) :class)))
|
||||||
|
(member "etaf-panel"
|
||||||
|
(if (stringp classes) (split-string classes) classes))))
|
||||||
|
(etaf-runtime-host-props-entries runtime)))
|
||||||
|
(ancestries (etaf-runtime-host-ancestries
|
||||||
|
runtime (cons control (mapcar #'car panels))))
|
||||||
|
(matches
|
||||||
|
(cl-remove-if-not
|
||||||
|
(lambda (entry)
|
||||||
|
(memq (car (gethash (car entry) ancestries))
|
||||||
|
(gethash control ancestries)))
|
||||||
|
panels)))
|
||||||
|
(should (= (length matches) 1))
|
||||||
|
(let* ((host (caar matches))
|
||||||
|
(bounds (etaf-host-ref-bounds runtime host)))
|
||||||
|
(should bounds)
|
||||||
|
(should (< (car bounds) (cdr bounds)))
|
||||||
|
(with-current-buffer (etaf-runtime-buffer runtime)
|
||||||
|
(let ((top (line-number-at-pos (car bounds)))
|
||||||
|
(bottom (line-number-at-pos (1- (cdr bounds)))))
|
||||||
|
(list :host host :bounds (copy-tree bounds)
|
||||||
|
:top top :bottom bottom :height (1+ (- bottom top))
|
||||||
|
:left
|
||||||
|
(save-excursion
|
||||||
|
(goto-char (car bounds))
|
||||||
|
(ebox-string-pixel-width
|
||||||
|
(buffer-substring (line-beginning-position) (point))))))))))
|
||||||
|
|
||||||
|
(cl-defmacro wb-test--with-app ((runtime data &optional viewport-width) &rest body)
|
||||||
|
"Mount RUNTIME and DATA at optional VIEWPORT-WIDTH, then execute BODY."
|
||||||
(declare (indent 1))
|
(declare (indent 1))
|
||||||
`(with-temp-buffer
|
`(with-temp-buffer
|
||||||
(let (,runtime ,data)
|
(let (,runtime ,data)
|
||||||
(unwind-protect
|
(unwind-protect
|
||||||
(progn
|
(progn
|
||||||
(etaf-mount (current-buffer) (etaf-view (wb-app))
|
(etaf-mount (current-buffer) (etaf-view (wb-app))
|
||||||
'(:viewport-width 1600 :viewport-height 80))
|
(list :viewport-width (or ,viewport-width 1600)
|
||||||
|
:viewport-height 80))
|
||||||
(setq ,runtime (etaf-runtime-for-buffer (current-buffer))
|
(setq ,runtime (etaf-runtime-for-buffer (current-buffer))
|
||||||
,data (wb-test--data ,runtime))
|
,data (wb-test--data ,runtime))
|
||||||
,@body)
|
,@body)
|
||||||
@ -288,6 +323,92 @@
|
|||||||
(should (equal widths (wb-test--line-widths runtime)))
|
(should (equal widths (wb-test--line-widths runtime)))
|
||||||
(should (wb-test--desktop-header-inline-p runtime)))))
|
(should (wb-test--desktop-header-inline-p runtime)))))
|
||||||
|
|
||||||
|
(ert-deftest wb-workbench-explanation-toggle-isolates-card-heights ()
|
||||||
|
"Explanation changes only detail height across three rendered layouts."
|
||||||
|
(dolist (viewport '((250 . stacked-sidebar) (500 . side-by-side-sidebar)
|
||||||
|
(70 . single-column)))
|
||||||
|
(ert-info ((format "Workbench explanation layout: %S" viewport))
|
||||||
|
(wb-test--with-app (runtime data (car viewport))
|
||||||
|
(etaf-dispatch-event runtime (wb-test--row runtime 1) 'press)
|
||||||
|
(cl-labels
|
||||||
|
((cards ()
|
||||||
|
(mapcar (lambda (entry)
|
||||||
|
(cons (car entry) (wb-test--card-layout runtime (cdr entry))))
|
||||||
|
'((filter . "全部") (tasks . "每页条数 10 ✎")
|
||||||
|
(detail . "显示说明") (service . "刷新 / 重试")))))
|
||||||
|
(let* ((before (cards))
|
||||||
|
(text (wb-test--text runtime))
|
||||||
|
(items (etaf-value (etaf-data-items data)))
|
||||||
|
(selected (etaf-data-selected-item data))
|
||||||
|
(detail (wb-test--instance runtime 'wb-detail))
|
||||||
|
(service (wb-test--instance runtime 'wb-service))
|
||||||
|
(expanded (plist-get (etaf--component-instance-state detail) :expanded))
|
||||||
|
(resource (plist-get (etaf--component-instance-state service) :resource))
|
||||||
|
(rows (mapcar (lambda (id) (wb-test--row runtime id))
|
||||||
|
(number-sequence 1 10)))
|
||||||
|
(control (wb-test--control runtime "显示说明"))
|
||||||
|
(filter (alist-get 'filter before))
|
||||||
|
(tasks (alist-get 'tasks before))
|
||||||
|
(detail-layout (alist-get 'detail before))
|
||||||
|
(service-layout (alist-get 'service before)))
|
||||||
|
;; Prove the viewport actually exercises the intended wrapping
|
||||||
|
;; shape from published host bounds, rather than style declarations.
|
||||||
|
(pcase (cdr viewport)
|
||||||
|
('stacked-sidebar
|
||||||
|
(should (= (plist-get filter :top) (plist-get tasks :top)
|
||||||
|
(plist-get detail-layout :top)))
|
||||||
|
(should (< (plist-get tasks :left) (plist-get detail-layout :left)))
|
||||||
|
(should (< (plist-get detail-layout :bottom)
|
||||||
|
(plist-get service-layout :top))))
|
||||||
|
('side-by-side-sidebar
|
||||||
|
(should (= (plist-get filter :top) (plist-get tasks :top)
|
||||||
|
(plist-get detail-layout :top) (plist-get service-layout :top)))
|
||||||
|
(should (< (plist-get detail-layout :left)
|
||||||
|
(plist-get service-layout :left))))
|
||||||
|
('single-column
|
||||||
|
(should (< (plist-get filter :bottom) (plist-get tasks :top)))
|
||||||
|
(should (< (plist-get tasks :bottom) (plist-get detail-layout :top)))
|
||||||
|
(should (< (plist-get detail-layout :bottom)
|
||||||
|
(plist-get service-layout :top)))))
|
||||||
|
(should (etaf-value expanded))
|
||||||
|
(should (= (wb-task-id selected) 1))
|
||||||
|
(should (string-match-p "点击行:选择任务" text))
|
||||||
|
(dolist (visible '(nil t))
|
||||||
|
(wb-test--press runtime "显示说明")
|
||||||
|
(let ((current (cards)))
|
||||||
|
(message "Workbench explanation width=%d visible=%S heights=%S -> %S"
|
||||||
|
(car viewport) visible
|
||||||
|
(mapcar (lambda (entry) (plist-get (cdr entry) :height)) before)
|
||||||
|
(mapcar (lambda (entry) (plist-get (cdr entry) :height)) current))
|
||||||
|
(dolist (card '(filter tasks service))
|
||||||
|
(should (= (plist-get (alist-get card before) :height)
|
||||||
|
(plist-get (alist-get card current) :height))))
|
||||||
|
(should (funcall (if visible #'= #'<)
|
||||||
|
(plist-get (alist-get 'detail current) :height)
|
||||||
|
(plist-get detail-layout :height)))
|
||||||
|
(dolist (card '(filter tasks detail service))
|
||||||
|
(should (equal (plist-get (alist-get card before) :host)
|
||||||
|
(plist-get (alist-get card current) :host))))
|
||||||
|
(should (eq visible (etaf-value expanded)))
|
||||||
|
(should (eq detail (wb-test--instance runtime 'wb-detail)))
|
||||||
|
(should (eq service (wb-test--instance runtime 'wb-service)))
|
||||||
|
(should (eq items (etaf-value (etaf-data-items data))))
|
||||||
|
(should (eq selected (etaf-data-selected-item data)))
|
||||||
|
(should (= (etaf-value (etaf-data-total data)) 100))
|
||||||
|
(should (= (etaf-value (etaf-data-page data)) 1))
|
||||||
|
(should (= (etaf-value (etaf-data-page-size data)) 10))
|
||||||
|
(should (eq (etaf-resource-status resource) 'success))
|
||||||
|
(should (equal rows (mapcar (lambda (id) (wb-test--row runtime id))
|
||||||
|
(number-sequence 1 10))))
|
||||||
|
(should (equal control (wb-test--control runtime "显示说明")))
|
||||||
|
(should (string-match-p "设计组件接口" (wb-test--text runtime)))
|
||||||
|
(should (string-match-p "服务正常" (wb-test--text runtime)))
|
||||||
|
(should (eq visible
|
||||||
|
(not (null (string-match-p "点击行:选择任务"
|
||||||
|
(wb-test--text runtime))))))))
|
||||||
|
(should (equal before (cards)))
|
||||||
|
(should (equal text (wb-test--text runtime)))))))))
|
||||||
|
|
||||||
(ert-deftest wb-workbench-theme-render-failure-retains-published-handlers ()
|
(ert-deftest wb-workbench-theme-render-failure-retains-published-handlers ()
|
||||||
"A failed theme render keeps the published UI and usable old callbacks."
|
"A failed theme render keeps the published UI and usable old callbacks."
|
||||||
(wb-test--with-app (runtime data)
|
(wb-test--with-app (runtime data)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user