From f4dd819f6a636fcb79edd9b047b88208b79c4758 Mon Sep 17 00:00:00 2001 From: 7800uk Date: Wed, 4 Nov 2026 00:11:34 +0000 Subject: [PATCH 1/8] test(showcase): use the option interface for renderer effects neomacs-effects-apply, neomacs-effect-set and neomacs-effect-reset were removed when effects became defcustoms, so the showcase signalled void-function from its first section. Set gallery and behavioural options with setopt. Use customize-set-variable and custom-reevaluate-setting where the effect is only known at run time, which is the Custom path the removal commit named as the replacement for reset. Keep neomacs--effects-apply for whole profiles, which no option expresses. Also drop the removed effects profile from the neomacs-cursor-effect docstring. --- lisp/term/neo-win.el | 2 +- test/neomacs/neomacs-showcase.el | 86 ++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/lisp/term/neo-win.el b/lisp/term/neo-win.el index a456d25103..e9a7505871 100644 --- a/lisp/term/neo-win.el +++ b/lisp/term/neo-win.el @@ -730,7 +730,7 @@ Applied at the render level." (defvar-local neomacs-cursor-effect nil "Per-buffer cursor effect profile. -The value is one entry or a list of entries in `neomacs-effects' format. +The value is one (EFFECT :PROPERTY VALUE ...) entry or a list of them. Only cursor effects are used when rendering this buffer's cursor.") ;; Provide the feature diff --git a/test/neomacs/neomacs-showcase.el b/test/neomacs/neomacs-showcase.el index 66fc4dc65c..c28d5b1452 100644 --- a/test/neomacs/neomacs-showcase.el +++ b/test/neomacs/neomacs-showcase.el @@ -4,6 +4,7 @@ (require 'neomacs-webkit nil t) (require 'neomacs-video nil t) (require 'neomacs-image nil t) +(require 'neomacs-effects nil t) (defvar showcase--log-file "/tmp/neomacs-showcase.log" "Path to the log file.") @@ -304,6 +305,19 @@ The future of text editing is here. It's called Neomacs. (push frame showcase--child-frames) frame)) +(defun showcase--effect-option (effect) + "Return the option `neomacs-effects' generates for gallery EFFECT." + (intern (format "neomacs-effect-%s" effect))) + +(defun showcase--effect-enable (effect) + "Turn on gallery EFFECT, for the callers that only know it at run time. +`setopt' needs a literal variable name; the rest of this file uses it." + (customize-set-variable (showcase--effect-option effect) '(:enabled t))) + +(defun showcase--effect-reset (effect) + "Restore gallery EFFECT's standard value, which also reaches the renderer." + (custom-reevaluate-setting (showcase--effect-option effect))) + (defun showcase--reset-all-effects () "Disable every visual effect for a clean slate." (showcase--log "RESET-ALL-EFFECTS called (step=%d)" showcase--step) @@ -319,7 +333,7 @@ The future of text editing is here. It's called Neomacs. (ignore-errors (neomacs-webkit-close-all)))) ;; Restore every renderer-defined default, then turn off the one shader ;; whose Rust default is intentionally enabled. - (neomacs-effects-apply '((cursor-color-cycle :enabled nil)))) + (neomacs--effects-apply '((cursor-color-cycle :enabled nil)))) (defun showcase--title-card (title &optional subtitle) "Show a title card with TITLE and optional SUBTITLE." @@ -504,9 +518,9 @@ The future of text editing is here. It's called Neomacs. (goto-char (+ (match-beginning 0) 1))) (redisplay t)))))))) ;; Breathing border (purple, slow) - (neomacs-effect-set 'breathing-border - :enabled t :color "#9966FF" - :min-opacity 0.05 :max-opacity 0.4 :cycle-ms 4000)) + (setopt neomacs-effect-breathing-border + '(:enabled t :color "#9966FF" + :min-opacity 0.05 :max-opacity 0.4 :cycle-ms 4000))) (defun showcase--section-cursor-anim () "Demonstrate smooth cursor with spring physics and glow." @@ -520,8 +534,10 @@ The future of text editing is here. It's called Neomacs. (recenter) (redisplay t) ;; Enable smooth cursor + glow + line highlight - (neomacs-effect-set 'cursor-glow :enabled t :color "#4D99FF" :radius 40) - (neomacs-effect-set 'line-highlight :enabled t) + (setopt neomacs-effect-cursor-glow + '(:enabled t :color "#4D99FF" :radius 40)) + (setopt neomacs-effect-line-highlight + '(:enabled t)) ;; Animate cursor through positions (let ((delay 0.3)) (showcase--schedule delay @@ -596,8 +612,8 @@ The future of text editing is here. It's called Neomacs. (showcase--schedule delay (lambda () (showcase--log " scroll effect: %s" effect) - (neomacs-effect-set 'scroll-transition - :enabled t :effect effect) + (setopt neomacs-scroll-transition-enabled t + neomacs-scroll-transition-effect effect) (message "Scroll Effect: %s" effect))) (showcase--schedule (+ delay 0.2) (lambda () @@ -640,10 +656,11 @@ The future of text editing is here. It's called Neomacs. (showcase--schedule delay (lambda () (when old - (neomacs-effect-reset old)) - (neomacs-effect-set effect :enabled t) + (showcase--effect-reset old)) + (showcase--effect-enable effect) (when (eq effect 'cursor-color-cycle) - (neomacs-effect-set 'cursor-glow :enabled t :radius 35)) + (setopt neomacs-effect-cursor-glow + '(:enabled t :radius 35))) (showcase--log " cursor effect: %s" name) (message "Cursor Effect: %s" name)))) (setq previous (cdr entry)) @@ -685,7 +702,7 @@ The future of text editing is here. It's called Neomacs. (profile (cadr entry))) (showcase--schedule delay (lambda () - (neomacs-effects-apply profile) + (neomacs--effects-apply profile) (showcase--log " atmosphere: %s" name) (message "Atmosphere: %s" name)))) (setq delay (+ delay 1.0))))) @@ -702,37 +719,40 @@ The future of text editing is here. It's called Neomacs. (let ((features '((0.2 "Line Numbers" (display-line-numbers-mode 1)) (0.8 "Rainbow Indent Guides" - (neomacs-effect-set 'indent-guides - :enabled t :rainbow-enabled t)) + (setopt neomacs-effect-indent-guides + '(:enabled t :rainbow-enabled t))) (1.4 "Line Highlight" - (neomacs-effect-set 'line-highlight :enabled t)) + (setopt neomacs-effect-line-highlight + '(:enabled t))) (2.0 "Visible Whitespace" - (neomacs-effect-set 'show-whitespace - :enabled t :color "#33FF66")) + (setopt neomacs-effect-show-whitespace + '(:enabled t :color "#33FF66"))) (2.6 "Minimap" - (neomacs-effect-set 'minimap :enabled t)) + (setopt neomacs-effect-minimap + '(:enabled t))) (3.2 "Focus Mode" - (neomacs-effect-set 'focus-mode - :enabled t :opacity 0.3)) + (setopt neomacs-effect-focus-mode + '(:enabled t :opacity 0.3))) (3.8 "Scroll Progress" - (neomacs-effect-set 'scroll-progress :enabled t)) + (setopt neomacs-effect-scroll-progress + '(:enabled t))) (4.5 "Split + Inactive Dimming" (progn (split-window-right) (other-window 1) (switch-to-buffer (showcase--insert-prose-buffer)) (other-window 1) - (neomacs-effect-set 'inactive-dim - :enabled t :opacity 0.2))) + (setopt neomacs-effect-inactive-dim + '(:enabled t :opacity 0.2)))) (5.5 "Zen Mode" (progn (delete-other-windows) - (neomacs-effect-reset 'focus-mode) - (neomacs-effect-reset 'minimap) + (showcase--effect-reset 'focus-mode) + (showcase--effect-reset 'minimap) (switch-to-buffer (showcase--insert-prose-buffer)) - (neomacs-effect-set 'zen-mode :enabled t - :content-width-pct 60 - :margin-opacity 0.3)))))) + (setopt neomacs-effect-zen-mode + '(:enabled t :content-width-pct 60 + :margin-opacity 0.3))))))) (dolist (feat features) (let ((d (nth 0 feat)) (name (nth 1 feat)) @@ -991,9 +1011,9 @@ The future of text editing is here. It's called Neomacs. (lambda () (showcase--log " window-trans: focus border") (message "Transition: Focus Gradient Border") - (neomacs-effect-set 'focus-gradient-border - :enabled t :top-color "#4D99FF" - :bot-color "#FF4DFF" :width 3 :opacity 0.8))) + (setopt neomacs-effect-focus-gradient-border + '(:enabled t :top-color "#4D99FF" + :bot-color "#FF4DFF" :width 3 :opacity 0.8)))) (showcase--schedule 4.3 (lambda () (other-window 1))) @@ -1240,7 +1260,7 @@ The future of text editing is here. It's called Neomacs. (insert (propertize "RETRO MODE ACTIVATED\n" 'face '(:height 2.0 :foreground "#4DFF4D" :weight bold))) (insert "\n"))) ;; Stack retro effects in one renderer update. - (neomacs-effects-apply + (neomacs--effects-apply '((scanlines :enabled t :spacing 3 :opacity 0.12 :color "#000000") (dot-matrix :enabled t) (bg-pattern :style 1 :spacing 16 :color "#003300" :opacity 0.08) @@ -1347,7 +1367,7 @@ The future of text editing is here. It's called Neomacs. (redisplay t)))))))) ;; Stack the prettiest effects. - (neomacs-effects-apply + (neomacs--effects-apply '((aurora :enabled t :color1 "#33CC66" :color2 "#9966FF" :height 100 :opacity 0.2) (neon-border :enabled t) From af0e223c4d539a2d96fb98050357894986d56da3 Mon Sep 17 00:00:00 2001 From: 7800uk Date: Wed, 4 Nov 2026 00:11:34 +0000 Subject: [PATCH 2/8] docs(effects): say which settings reach the renderer Options reach the renderer only through :set, so setq on a loaded option changes the variable alone: in a batch run, scroll-transition stayed disabled after setq and turned on after setopt. Correct the commentary and the two transition defaults that suggested setq, rather than add watchers. A watcher on 210 options would also fire on let-binding and on the set-default inside every :set, and setq on a :set option does nothing in GNU Emacs either. Rename the private primitives in Rust comments and drop the removed neomacs-effects profile. --- .../tests/child_frame_animation_test.rs | 2 +- .../src/effect_config/tests/effect_config_test.rs | 4 ++-- crates/neomacs-display-protocol/src/motion_spec.rs | 2 +- .../src/tests/effect_config_test.rs | 4 ++-- .../neomacs-display-protocol/src/visual_config.rs | 14 ++++++++------ .../src/window_animation.rs | 2 +- lisp/neomacs-effects.el | 7 +++++-- 7 files changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/neomacs-display-protocol/src/child_frame_animation/tests/child_frame_animation_test.rs b/crates/neomacs-display-protocol/src/child_frame_animation/tests/child_frame_animation_test.rs index 1085f8154f..1177d7fb72 100644 --- a/crates/neomacs-display-protocol/src/child_frame_animation/tests/child_frame_animation_test.rs +++ b/crates/neomacs-display-protocol/src/child_frame_animation/tests/child_frame_animation_test.rs @@ -121,7 +121,7 @@ fn a_cubic_bezier_slot_carries_its_control_points_into_the_spec() { #[test] fn the_registry_carries_every_slot_and_the_slide_property() { - // `neomacs-effect-set` validates a property key against the value already + // `neomacs--effect-set` validates a property key against the value already // stored, so a property missing from the serialized shape makes switching // it from Lisp unreachable. Walking every name of the config struct and // round-tripping one value through the registry proves the opposite. diff --git a/crates/neomacs-display-protocol/src/effect_config/tests/effect_config_test.rs b/crates/neomacs-display-protocol/src/effect_config/tests/effect_config_test.rs index fc95a34974..14dac3ff6e 100644 --- a/crates/neomacs-display-protocol/src/effect_config/tests/effect_config_test.rs +++ b/crates/neomacs-display-protocol/src/effect_config/tests/effect_config_test.rs @@ -2648,7 +2648,7 @@ fn every_effect_in_the_registry_can_report_its_values() { // The registry is pure serde reflection over `VisualConfig`, and a property // that serializes to an object (anything but the `Duration` special case) // makes `effect_values` fail for the *whole* effect — which takes - // `neomacs-effect-get` down for it and `neomacs-effects-apply`, which walks + // `neomacs-effect-get` down for it and `neomacs--effects-apply`, which walks // every effect, down entirely. That failure is invisible from the Rust side // until someone asks from Lisp, so it is asserted here instead. let config = VisualConfig::default(); @@ -2730,7 +2730,7 @@ fn every_window_animation_slot_reaches_the_registry_as_scalars() { // break if a slot stored a `MotionSpec` directly: every variant but // `Instant` serializes to an object, which the registry cannot represent, // so `neomacs-effect-get` would fail for the slot — and - // `neomacs-effects-apply`, which walks every effect, with it. + // `neomacs--effects-apply`, which walks every effect, with it. assert!(sprung.effect_values("window-open").is_ok()); } diff --git a/crates/neomacs-display-protocol/src/motion_spec.rs b/crates/neomacs-display-protocol/src/motion_spec.rs index be9fd2a614..1916357fff 100644 --- a/crates/neomacs-display-protocol/src/motion_spec.rs +++ b/crates/neomacs-display-protocol/src/motion_spec.rs @@ -275,7 +275,7 @@ impl<'de> serde::Deserialize<'de> for DevicePixels { /// that type is a *registry* field on four different configs, and the effect /// registry can only carry scalar property values. A data-carrying easing /// variant would serialize to an object and break `neomacs-effect-get` for -/// whichever slot selected it -- and `neomacs-effects-apply`, which walks every +/// whichever slot selected it -- and `neomacs--effects-apply`, which walks every /// effect, along with it. Keeping `TransitionEasing` fieldless makes that /// impossible by construction rather than by discipline. #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] diff --git a/crates/neomacs-display-protocol/src/tests/effect_config_test.rs b/crates/neomacs-display-protocol/src/tests/effect_config_test.rs index fc95a34974..14dac3ff6e 100644 --- a/crates/neomacs-display-protocol/src/tests/effect_config_test.rs +++ b/crates/neomacs-display-protocol/src/tests/effect_config_test.rs @@ -2648,7 +2648,7 @@ fn every_effect_in_the_registry_can_report_its_values() { // The registry is pure serde reflection over `VisualConfig`, and a property // that serializes to an object (anything but the `Duration` special case) // makes `effect_values` fail for the *whole* effect — which takes - // `neomacs-effect-get` down for it and `neomacs-effects-apply`, which walks + // `neomacs-effect-get` down for it and `neomacs--effects-apply`, which walks // every effect, down entirely. That failure is invisible from the Rust side // until someone asks from Lisp, so it is asserted here instead. let config = VisualConfig::default(); @@ -2730,7 +2730,7 @@ fn every_window_animation_slot_reaches_the_registry_as_scalars() { // break if a slot stored a `MotionSpec` directly: every variant but // `Instant` serializes to an object, which the registry cannot represent, // so `neomacs-effect-get` would fail for the slot — and - // `neomacs-effects-apply`, which walks every effect, with it. + // `neomacs--effects-apply`, which walks every effect, with it. assert!(sprung.effect_values("window-open").is_ok()); } diff --git a/crates/neomacs-display-protocol/src/visual_config.rs b/crates/neomacs-display-protocol/src/visual_config.rs index 4959a16bed..2415e73063 100644 --- a/crates/neomacs-display-protocol/src/visual_config.rs +++ b/crates/neomacs-display-protocol/src/visual_config.rs @@ -92,9 +92,10 @@ impl Default for BufferTransitionConfig { // Off by default: switching buffers repaints instantly, matching // stock Emacs, the same choice ScrollTransitionConfig makes below. // Opt in at runtime with - // (setq neomacs-buffer-transition-enabled t) - // or via the `neomacs-effects' profile. The effect/duration/easing - // below are the values used once it is enabled. + // (setopt neomacs-buffer-transition-enabled t) + // or through Customize; a plain `setq' never reaches the renderer. + // The effect/duration/easing below are the values used once it is + // enabled. enabled: false, duration: Duration::from_millis(200), effect: TransitionEffect::Slide, @@ -118,9 +119,10 @@ impl Default for ScrollTransitionConfig { Self { // Off by default: C-v/M-v (and other scrolls) update instantly, // matching stock Emacs. Opt in at runtime with - // (setq neomacs-scroll-transition-enabled t) - // or via the `neomacs-effects' profile. The effect/duration/easing - // below are the values used once it is enabled. + // (setopt neomacs-scroll-transition-enabled t) + // or through Customize; a plain `setq' never reaches the renderer. + // The effect/duration/easing below are the values used once it is + // enabled. enabled: false, duration: Duration::from_millis(150), effect: TransitionEffect::Slide, diff --git a/crates/neomacs-display-protocol/src/window_animation.rs b/crates/neomacs-display-protocol/src/window_animation.rs index 0c4b7caaf9..bb548f3a5f 100644 --- a/crates/neomacs-display-protocol/src/window_animation.rs +++ b/crates/neomacs-display-protocol/src/window_animation.rs @@ -13,7 +13,7 @@ //! The effect registry reflects [`crate::visual_config::VisualConfig`] through //! serde and can carry only scalar property values. Every `MotionSpec` variant //! but `Instant` serializes to an *object*, so storing one here would make -//! `neomacs-effect-get` fail for the slot -- and `neomacs-effects-apply`, which +//! `neomacs-effect-get` fail for the slot -- and `neomacs--effects-apply`, which //! walks every effect, with it. [`WindowAnimation::motion`] converts to the //! form the compositor samples. //! diff --git a/lisp/neomacs-effects.el b/lisp/neomacs-effects.el index 274c5960ad..0aa3d03f38 100644 --- a/lisp/neomacs-effects.el +++ b/lisp/neomacs-effects.el @@ -6,8 +6,11 @@ ;; Every renderer effect is configured through `defcustom', the way ;; `blink-cursor-interval' and friends are: one option per property, with a -;; type, a docstring and a group. There is no public setter function. Setting -;; an option with `setq' or through \\[customize] is the whole interface. +;; type, a docstring and a group. There is no public setter function: `setopt' +;; and \\[customize] are the whole interface, because an option reaches the +;; renderer through its `:set' function. As with any GNU option that has one, a +;; plain `setq' once this file is loaded changes the variable and nothing else; +;; a value already set when this file loads is picked up by the `defcustom'. ;; ;; The properties themselves are defined in Rust and published by ;; `neomacs-effect-schema', which reports each property's kind, its bounds and, From 048f65d373807d5962f66e6f0d6ffcf7cefc81f1 Mon Sep 17 00:00:00 2001 From: 7800uk Date: Wed, 4 Nov 2026 00:11:34 +0000 Subject: [PATCH 3/8] fix(perf): probe the xwidget feature for webview capability neomacs-webkit-create is not registered by any crate, so every artifact reported webview as false. xwidget-internal is what the runtime provides when the webview backend is present. --- crates/neomacs-perf/src/harness.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/neomacs-perf/src/harness.rs b/crates/neomacs-perf/src/harness.rs index 735dacacb4..d9a2c88b3a 100644 --- a/crates/neomacs-perf/src/harness.rs +++ b/crates/neomacs-perf/src/harness.rs @@ -1574,7 +1574,7 @@ fn editor_capabilities( "--batch", "-Q", "--eval", - r#"(princ (format "%d,%d,%d,%d,%d,%d" (if (and (fboundp 'native-comp-available-p) (native-comp-available-p)) 1 0) (if (and (fboundp 'treesit-available-p) (treesit-available-p)) 1 0) (if (fboundp 'module-load) 1 0) (if (fboundp 'neomacs-video-load) 1 0) (if (fboundp 'neomacs-webkit-create) 1 0) (if (fboundp 'neomacs-terminal-create) 1 0)))"#, + r#"(princ (format "%d,%d,%d,%d,%d,%d" (if (and (fboundp 'native-comp-available-p) (native-comp-available-p)) 1 0) (if (and (fboundp 'treesit-available-p) (treesit-available-p)) 1 0) (if (fboundp 'module-load) 1 0) (if (fboundp 'neomacs-video-load) 1 0) (if (featurep 'xwidget-internal) 1 0) (if (fboundp 'neomacs-terminal-create) 1 0)))"#, ]); let output = command .output() From 900100da707a03348c7ccd6d93b5a1f58c9795e4 Mon Sep 17 00:00:00 2001 From: 7800uk Date: Wed, 4 Nov 2026 00:42:28 +0000 Subject: [PATCH 4/8] fix(effects): stop offering cubic-bezier for buffer and scroll easing BufferTransitionConfig and ScrollTransitionConfig have nowhere to store control points, so choosing cubic-bezier silently ran as linear. The window and child-frame slots keep the choice because they carry the points. --- lisp/neomacs-effects.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/neomacs-effects.el b/lisp/neomacs-effects.el index 0aa3d03f38..01d53b149a 100644 --- a/lisp/neomacs-effects.el +++ b/lisp/neomacs-effects.el @@ -537,7 +537,7 @@ decorative." "Curve shape for a buffer switch." :type '(choice (const linear) (const ease-out-quad) (const ease-out-cubic) (const ease-out-expo) (const ease-in-out-cubic) - (const spring) (const cubic-bezier)) + (const spring)) :group 'neomacs-transitions :set (lambda (symbol value) (set-default symbol value) @@ -597,7 +597,7 @@ When nil the viewport jumps to its new position on the next frame." "Curve shape for a scroll." :type '(choice (const linear) (const ease-out-quad) (const ease-out-cubic) (const ease-out-expo) (const ease-in-out-cubic) - (const spring) (const cubic-bezier)) + (const spring)) :group 'neomacs-transitions :set (lambda (symbol value) (set-default symbol value) From 37c809881da4786a63e2caacf7e51344c717e4c3 Mon Sep 17 00:00:00 2001 From: 7800uk Date: Wed, 4 Nov 2026 08:27:39 +0000 Subject: [PATCH 5/8] Revert "fix(perf): probe the xwidget feature for webview capability" This reverts commit 1c13d7ce5b0863c7fafbbadeba88c8b53c48853c. --- crates/neomacs-perf/src/harness.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/neomacs-perf/src/harness.rs b/crates/neomacs-perf/src/harness.rs index d9a2c88b3a..735dacacb4 100644 --- a/crates/neomacs-perf/src/harness.rs +++ b/crates/neomacs-perf/src/harness.rs @@ -1574,7 +1574,7 @@ fn editor_capabilities( "--batch", "-Q", "--eval", - r#"(princ (format "%d,%d,%d,%d,%d,%d" (if (and (fboundp 'native-comp-available-p) (native-comp-available-p)) 1 0) (if (and (fboundp 'treesit-available-p) (treesit-available-p)) 1 0) (if (fboundp 'module-load) 1 0) (if (fboundp 'neomacs-video-load) 1 0) (if (featurep 'xwidget-internal) 1 0) (if (fboundp 'neomacs-terminal-create) 1 0)))"#, + r#"(princ (format "%d,%d,%d,%d,%d,%d" (if (and (fboundp 'native-comp-available-p) (native-comp-available-p)) 1 0) (if (and (fboundp 'treesit-available-p) (treesit-available-p)) 1 0) (if (fboundp 'module-load) 1 0) (if (fboundp 'neomacs-video-load) 1 0) (if (fboundp 'neomacs-webkit-create) 1 0) (if (fboundp 'neomacs-terminal-create) 1 0)))"#, ]); let output = command .output() From 99b7c75724b6a0868ba2d9f72fbc9ddfe2d1f639 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 7 Nov 2026 10:52:07 +0000 Subject: [PATCH 6/8] test(showcase): keep demo effects off the user's options The showcase turned gallery effects on with customize-set-variable and put them back with custom-reevaluate-setting. The first records a customization; the second reapplies a saved value, so leaving an effect turned a saved one back on. Section resets published a clean renderer through neomacs--effects-apply and left the options at the demo values. Remember the renderer and the option values before the first change. Drive the demo through each option's :set function, and on quit write the snapshot back with set-default so neither customized-value nor a saved value is involved. Co-authored-by: Eval Exec --- test/neomacs/neomacs-showcase-effects-test.el | 222 +++++++++++++++++ test/neomacs/neomacs-showcase.el | 234 ++++++++++++++---- 2 files changed, 405 insertions(+), 51 deletions(-) create mode 100644 test/neomacs/neomacs-showcase-effects-test.el diff --git a/test/neomacs/neomacs-showcase-effects-test.el b/test/neomacs/neomacs-showcase-effects-test.el new file mode 100644 index 0000000000..ea5b1e78e4 --- /dev/null +++ b/test/neomacs/neomacs-showcase-effects-test.el @@ -0,0 +1,222 @@ +;;; neomacs-showcase-effects-test.el --- transient effect session regression -*- lexical-binding: t; -*- + +;; Run against the pinned GNU Emacs from parity-reference.toml: +;; +;; src/emacs -Q --batch -l test/neomacs/neomacs-showcase-effects-test.el +;; +;; The Neomacs renderer subrs are stubbed with the same two rules the Rust +;; side uses: `neomacs--effect-set' merges properties onto the current +;; config, and `neomacs--effects-apply' starts from the Rust defaults and +;; then applies the profile. `cursor-color-cycle' defaults to enabled. + +(defvar showcase-effects-test-failures nil) + +(defun showcase-effects-test-fail (fmt &rest args) + (let ((msg (apply #'format fmt args))) + (push msg showcase-effects-test-failures) + (princ (format "FAIL %s\n" msg)))) + +(defun showcase-effects-test-enabled (plist) + (plist-get plist :enabled)) + +(defun showcase-effects-test-reset-session () + (when (boundp 'showcase--saved-renderer) + (setq showcase--saved-renderer nil)) + (when (boundp 'showcase--saved-options) + (setq showcase--saved-options nil))) + +(defun showcase-effects-test-custom-clean (symbol) + (put symbol 'customized-value nil) + (put symbol 'theme-value nil) + (put symbol 'saved-value nil) + (put symbol 'custom-check-value nil)) + +(defvar neo-test-config (make-hash-table :test #'eq)) + +(defun neo-test-default (effect) + (if (eq effect 'cursor-color-cycle) + (list :enabled t) + (list :enabled nil))) + +(defun neomacs-effect-names (&optional scope) + (let ((shaders '(aurora breathing-border cursor-color-cycle cursor-firework + cursor-glow focus-gradient-border focus-mode + inactive-dim indent-guides line-highlight minimap + scroll-progress show-whitespace zen-mode)) + (behavior '(buffer-transition cursor-blink cursor-motion + cursor-size-transition scroll-transition + window-animations window-close + window-movement window-open + window-resize child-frame-animations + child-frame-close child-frame-movement + child-frame-open child-frame-resize))) + (cond ((eq scope 'shader) shaders) + ((eq scope 'behavior) behavior) + ((eq scope 'cursor) '(cursor-color-cycle cursor-firework cursor-glow)) + (t (append shaders behavior))))) + +(defun neomacs-effect-schema (effect) + ;; These two publish :off / :slowdown, not :enabled. A stub :enabled + ;; entry makes `neomacs-effects--check-schema' warn about a symbol the + ;; real file deliberately does not define. + (if (memq effect '(window-animations child-frame-animations)) + nil + '((:enabled boolean)))) + +(defun neomacs-effect-get (effect) + (copy-sequence (or (gethash effect neo-test-config) + (neo-test-default effect)))) + +(defun neomacs--effect-set (effect &rest plist) + (let ((current (neomacs-effect-get effect))) + (while plist + (setq current (plist-put current (car plist) (cadr plist))) + (setq plist (cddr plist))) + (puthash effect current neo-test-config) + nil)) + +(defun neomacs--effects-apply (profile) + (clrhash neo-test-config) + (dolist (effect (neomacs-effect-names)) + (puthash effect (neo-test-default effect) neo-test-config)) + (dolist (entry profile) + (apply #'neomacs--effect-set entry))) + +(defun showcase-effects-test-user-set (symbol value) + (let ((setter (get symbol 'custom-set))) + (if setter + (funcall setter symbol value) + (set-default symbol value)))) + +(let* ((root (expand-file-name "../.." (file-name-directory load-file-name))) + (lisp (expand-file-name "lisp" root))) + ;; Load Neomacs's effects library by path. Do not put its `lisp/' + ;; directory on `load-path': that tree replaces GNU macros such as + ;; `defun' and the showcase then fails to expand. + (setq showcase--inhibit-init t) + (load (expand-file-name "neomacs-effects.el" lisp)) + (load (expand-file-name "test/neomacs/neomacs-showcase.el" root))) + +(unless (boundp 'neomacs-effect-cursor-firework) + (showcase-effects-test-fail "gallery option neomacs-effect-cursor-firework was not defined")) + +;; A transient enable must not become a user customization, and leaving the +;; effect must turn it off even when a saved Custom value says enabled. +(let ((sym 'neomacs-effect-cursor-firework)) + (showcase-effects-test-reset-session) + (showcase-effects-test-custom-clean sym) + (showcase-effects-test-user-set sym '(:enabled nil)) + (put sym 'saved-value (list (list 'quote '(:enabled t)))) + (showcase--effect-enable 'cursor-firework) + (when (get sym 'customized-value) + (showcase-effects-test-fail + "enable stored customized-value %S" (get sym 'customized-value))) + (when (get sym 'theme-value) + (showcase-effects-test-fail + "enable stored theme-value %S" (get sym 'theme-value))) + (unless (eq (showcase-effects-test-enabled (symbol-value sym)) t) + (showcase-effects-test-fail "enable left option %S" (symbol-value sym))) + (unless (eq (showcase-effects-test-enabled (neomacs-effect-get 'cursor-firework)) t) + (showcase-effects-test-fail + "enable left renderer %S" (neomacs-effect-get 'cursor-firework))) + (showcase--effect-reset 'cursor-firework) + (when (eq (showcase-effects-test-enabled (neomacs-effect-get 'cursor-firework)) t) + (showcase-effects-test-fail + "reset re-enabled firework from saved-value; renderer %S option %S" + (neomacs-effect-get 'cursor-firework) (symbol-value sym))) + (unless (equal (get sym 'saved-value) (list (list 'quote '(:enabled t)))) + (showcase-effects-test-fail "reset rewrote saved-value %S" (get sym 'saved-value))) + (when (get sym 'customized-value) + (showcase-effects-test-fail + "reset stored customized-value %S" (get sym 'customized-value)))) + +;; A section reset must leave the option and the renderer describing the +;; same effect, and quitting must put the user's value back in both places. +(let ((sym 'neomacs-effect-breathing-border) + (user '(:enabled t :color "#112233"))) + (showcase-effects-test-reset-session) + (showcase-effects-test-custom-clean sym) + (showcase-effects-test-user-set sym user) + (showcase--reset-all-effects) + (let ((option (symbol-value sym)) + (renderer (neomacs-effect-get 'breathing-border))) + (unless (eq (showcase-effects-test-enabled option) + (showcase-effects-test-enabled renderer)) + (showcase-effects-test-fail + "reset-all desynced breathing-border option %S renderer %S" + option renderer))) + (when (get sym 'customized-value) + (showcase-effects-test-fail + "reset-all stored customized-value %S" (get sym 'customized-value))) + (showcase-quit) + (let ((option (symbol-value sym)) + (renderer (neomacs-effect-get 'breathing-border))) + (unless (and (eq (showcase-effects-test-enabled option) t) + (equal (plist-get option :color) "#112233")) + (showcase-effects-test-fail "quit left option %S" option)) + (unless (and (eq (showcase-effects-test-enabled renderer) t) + (equal (plist-get renderer :color) "#112233")) + (showcase-effects-test-fail "quit left renderer %S" renderer))) + (when (get sym 'customized-value) + (showcase-effects-test-fail + "quit stored customized-value %S" (get sym 'customized-value)))) + +;; The one default-on shader is off for a clean section and back on after quit. +(let ((sym 'neomacs-effect-cursor-color-cycle)) + (showcase-effects-test-reset-session) + (showcase-effects-test-custom-clean sym) + (showcase-effects-test-user-set sym '(:enabled t)) + (showcase--reset-all-effects) + (when (or (showcase-effects-test-enabled (symbol-value sym)) + (showcase-effects-test-enabled (neomacs-effect-get 'cursor-color-cycle))) + (showcase-effects-test-fail + "clean slate left color-cycle option %S renderer %S" + (symbol-value sym) (neomacs-effect-get 'cursor-color-cycle))) + (showcase-quit) + (unless (and (eq (showcase-effects-test-enabled (symbol-value sym)) t) + (eq (showcase-effects-test-enabled + (neomacs-effect-get 'cursor-color-cycle)) + t)) + (showcase-effects-test-fail + "quit did not restore color-cycle option %S renderer %S" + (symbol-value sym) (neomacs-effect-get 'cursor-color-cycle)))) + +;; A profile must not turn the renderer on while leaving the option off, +;; and it must not wipe an effect the profile does not mention. +(if (not (fboundp 'showcase--apply-profile)) + (showcase-effects-test-fail "showcase--apply-profile is not defined") + (let ((highlight 'neomacs-effect-line-highlight) + (aurora 'neomacs-effect-aurora)) + (showcase-effects-test-reset-session) + (showcase-effects-test-custom-clean highlight) + (showcase-effects-test-custom-clean aurora) + (showcase-effects-test-user-set highlight '(:enabled t)) + (showcase--reset-all-effects) + (showcase--apply-profile '((aurora :enabled t :opacity 0.2))) + (when (or (showcase-effects-test-enabled (symbol-value highlight)) + (showcase-effects-test-enabled (neomacs-effect-get 'line-highlight))) + (showcase-effects-test-fail + "profile resurrected line-highlight option %S renderer %S" + (symbol-value highlight) (neomacs-effect-get 'line-highlight))) + (unless (and (eq (showcase-effects-test-enabled (symbol-value aurora)) t) + (eq (showcase-effects-test-enabled (neomacs-effect-get 'aurora)) t)) + (showcase-effects-test-fail + "profile desynced aurora option %S renderer %S" + (symbol-value aurora) (neomacs-effect-get 'aurora))) + (when (get aurora 'customized-value) + (showcase-effects-test-fail + "profile stored customized-value %S" (get aurora 'customized-value))) + (showcase-quit) + (unless (and (eq (showcase-effects-test-enabled (symbol-value highlight)) t) + (eq (showcase-effects-test-enabled (neomacs-effect-get 'line-highlight)) t)) + (showcase-effects-test-fail + "quit lost line-highlight option %S renderer %S" + (symbol-value highlight) (neomacs-effect-get 'line-highlight))) + (when (showcase-effects-test-enabled (neomacs-effect-get 'aurora)) + (showcase-effects-test-fail + "quit left aurora renderer %S" (neomacs-effect-get 'aurora))))) + +(when showcase-effects-test-failures + (princ (format "FAILED %d\n" (length showcase-effects-test-failures))) + (kill-emacs 1)) +(princ "PASS\n") diff --git a/test/neomacs/neomacs-showcase.el b/test/neomacs/neomacs-showcase.el index c28d5b1452..94dcabcc92 100644 --- a/test/neomacs/neomacs-showcase.el +++ b/test/neomacs/neomacs-showcase.el @@ -305,22 +305,145 @@ The future of text editing is here. It's called Neomacs. (push frame showcase--child-frames) frame)) +(defvar showcase--saved-renderer nil + "Renderer plists captured before this demo changes an effect. +Nil means no session is open. Each element is (EFFECT . PLIST), as +returned by `neomacs-effect-get'.") + +(defvar showcase--saved-options nil + "Option values captured with `showcase--saved-renderer'. +Each element is (SYMBOL . VALUE). Restoring writes these with +`set-default' so a demo cannot mark them customized.") + (defun showcase--effect-option (effect) "Return the option `neomacs-effects' generates for gallery EFFECT." (intern (format "neomacs-effect-%s" effect))) +(defun showcase--plist-merge (base overlay) + "Return a plist of BASE with OVERLAY's properties written over it." + (setq base (copy-sequence (if (listp base) base nil))) + (while overlay + (setq base (plist-put base (car overlay) (cadr overlay))) + (setq overlay (cddr overlay))) + base) + +(defun showcase--each-effect-option (function) + "Call FUNCTION with each effect option that has a `:set' function." + (when (and (fboundp 'neomacs-effect-names) (fboundp 'neomacs-effect-get)) + (dolist (effect (neomacs-effect-names)) + (let ((gallery (showcase--effect-option effect))) + (if (and (boundp gallery) (get gallery 'custom-set)) + (funcall function gallery) + (let ((plist (neomacs-effect-get effect))) + (while plist + (let ((symbol (intern (format "neomacs-%s-%s" + effect + (substring (symbol-name (car plist)) 1))))) + (when (and (boundp symbol) (get symbol 'custom-set)) + (funcall function symbol))) + (setq plist (cddr plist))))))))) + +(defun showcase--begin-effect-session () + "Remember the renderer and the option values before the first demo change." + (when (and (not showcase--saved-renderer) + (fboundp 'neomacs-effect-names) + (fboundp 'neomacs-effect-get) + (fboundp 'neomacs--effects-apply)) + (setq showcase--saved-renderer + (mapcar (lambda (effect) + (cons effect (neomacs-effect-get effect))) + (neomacs-effect-names))) + (setq showcase--saved-options nil) + (showcase--each-effect-option + (lambda (symbol) + (push (cons symbol (symbol-value symbol)) showcase--saved-options))))) + +(defun showcase--install-renderer-into-options () + "Copy the current renderer into the option variables. +Uses `set-default', not an option's `:set' function: the renderer was +just published, and `:set' would publish it again and could record a +customization." + (when (and (fboundp 'neomacs-effect-names) (fboundp 'neomacs-effect-get)) + (dolist (effect (neomacs-effect-names)) + (let ((plist (neomacs-effect-get effect)) + (gallery (showcase--effect-option effect))) + (if (and (boundp gallery) (get gallery 'custom-set)) + (set-default gallery (copy-sequence plist)) + (while plist + (let ((symbol (intern (format "neomacs-%s-%s" + effect + (substring (symbol-name (car plist)) 1))))) + (when (and (boundp symbol) (get symbol 'custom-set)) + (set-default symbol (cadr plist)))) + (setq plist (cddr plist)))))))) + +(defun showcase--option-set (symbol value) + "Set option SYMBOL to VALUE through its `:set' function. +This updates the variable and the renderer together. It does not call +`customize-set-variable' or `custom-reevaluate-setting', so the change +is not saved as the user's customization and a saved value is not +reapplied." + (showcase--begin-effect-session) + (let ((setter (get symbol 'custom-set))) + (if setter + (funcall setter symbol value) + (set-default symbol value)))) + +(defun showcase--gallery-set (effect plist) + "Set gallery EFFECT to PLIST, keeping its other properties. +When the effect has an option, the option and the renderer receive the +merged plist. Otherwise only the renderer changes." + (let ((symbol (showcase--effect-option effect))) + (if (and (boundp symbol) (get symbol 'custom-set)) + (showcase--option-set + symbol + (showcase--plist-merge (and (listp (symbol-value symbol)) + (symbol-value symbol)) + plist)) + (showcase--begin-effect-session) + (when (fboundp 'neomacs--effect-set) + (apply #'neomacs--effect-set effect plist))))) + +(defun showcase--apply-profile (profile) + "Apply each entry of PROFILE with `showcase--gallery-set'. +PROFILE has the same shape `neomacs--effects-apply' accepts. Unlike +that subr, this does not rebuild every effect from the Rust defaults, +so options the profile does not mention stay in sync with the renderer." + (dolist (entry profile) + (showcase--gallery-set (car entry) (cdr entry)))) + (defun showcase--effect-enable (effect) - "Turn on gallery EFFECT, for the callers that only know it at run time. -`setopt' needs a literal variable name; the rest of this file uses it." - (customize-set-variable (showcase--effect-option effect) '(:enabled t))) + "Turn on gallery EFFECT without recording a user customization." + (showcase--gallery-set effect '(:enabled t))) (defun showcase--effect-reset (effect) - "Restore gallery EFFECT's standard value, which also reaches the renderer." - (custom-reevaluate-setting (showcase--effect-option effect))) - -(defun showcase--reset-all-effects () - "Disable every visual effect for a clean slate." - (showcase--log "RESET-ALL-EFFECTS called (step=%d)" showcase--step) + "Turn gallery EFFECT off for the demo, ignoring any saved Custom value. +The value from before the demo is put back by `showcase-quit'." + (showcase--gallery-set effect '(:enabled nil))) + +(defun showcase--demo-clean-effects () + "Make the renderer, and the options, a clean demo slate. +The slate is the Rust defaults with `cursor-color-cycle' forced off. +The pre-demo renderer and option values stay in the session snapshot." + (showcase--begin-effect-session) + (when (fboundp 'neomacs--effects-apply) + (neomacs--effects-apply '((cursor-color-cycle :enabled nil))) + (showcase--install-renderer-into-options))) + +(defun showcase--end-effect-session () + "Put back the renderer and the options captured for this demo." + (when showcase--saved-renderer + (neomacs--effects-apply + (mapcar (lambda (entry) + (cons (car entry) (copy-sequence (cdr entry)))) + showcase--saved-renderer)) + (dolist (cell showcase--saved-options) + (set-default (car cell) (cdr cell))) + (setq showcase--saved-renderer nil + showcase--saved-options nil))) + +(defun showcase--reset-demo-chrome () + "Drop timers, child frames, and extra windows used by the demo." (showcase--cancel-timers) (showcase--cleanup-child-frames) (delete-other-windows) @@ -330,10 +453,15 @@ The future of text editing is here. It's called Neomacs. (ignore-errors (neomacs-video-stop-all)))) (when (fboundp 'neomacs-webkit-close-all) (let ((inhibit-message t)) - (ignore-errors (neomacs-webkit-close-all)))) - ;; Restore every renderer-defined default, then turn off the one shader - ;; whose Rust default is intentionally enabled. - (neomacs--effects-apply '((cursor-color-cycle :enabled nil)))) + (ignore-errors (neomacs-webkit-close-all))))) + +(defun showcase--reset-all-effects () + "Disable every visual effect for a clean slate. +The user's options and the pre-demo renderer are remembered and put +back by `showcase-quit'. Nothing here is recorded as a customization." + (showcase--log "RESET-ALL-EFFECTS called (step=%d)" showcase--step) + (showcase--reset-demo-chrome) + (showcase--demo-clean-effects)) (defun showcase--title-card (title &optional subtitle) "Show a title card with TITLE and optional SUBTITLE." @@ -518,9 +646,10 @@ The future of text editing is here. It's called Neomacs. (goto-char (+ (match-beginning 0) 1))) (redisplay t)))))))) ;; Breathing border (purple, slow) - (setopt neomacs-effect-breathing-border - '(:enabled t :color "#9966FF" - :min-opacity 0.05 :max-opacity 0.4 :cycle-ms 4000))) + (showcase--gallery-set + 'breathing-border + '(:enabled t :color "#9966FF" + :min-opacity 0.05 :max-opacity 0.4 :cycle-ms 4000))) (defun showcase--section-cursor-anim () "Demonstrate smooth cursor with spring physics and glow." @@ -534,10 +663,9 @@ The future of text editing is here. It's called Neomacs. (recenter) (redisplay t) ;; Enable smooth cursor + glow + line highlight - (setopt neomacs-effect-cursor-glow - '(:enabled t :color "#4D99FF" :radius 40)) - (setopt neomacs-effect-line-highlight - '(:enabled t)) + (showcase--gallery-set 'cursor-glow + '(:enabled t :color "#4D99FF" :radius 40)) + (showcase--gallery-set 'line-highlight '(:enabled t)) ;; Animate cursor through positions (let ((delay 0.3)) (showcase--schedule delay @@ -612,8 +740,8 @@ The future of text editing is here. It's called Neomacs. (showcase--schedule delay (lambda () (showcase--log " scroll effect: %s" effect) - (setopt neomacs-scroll-transition-enabled t - neomacs-scroll-transition-effect effect) + (showcase--option-set 'neomacs-scroll-transition-enabled t) + (showcase--option-set 'neomacs-scroll-transition-effect effect) (message "Scroll Effect: %s" effect))) (showcase--schedule (+ delay 0.2) (lambda () @@ -659,8 +787,7 @@ The future of text editing is here. It's called Neomacs. (showcase--effect-reset old)) (showcase--effect-enable effect) (when (eq effect 'cursor-color-cycle) - (setopt neomacs-effect-cursor-glow - '(:enabled t :radius 35))) + (showcase--gallery-set 'cursor-glow '(:enabled t :radius 35))) (showcase--log " cursor effect: %s" name) (message "Cursor Effect: %s" name)))) (setq previous (cdr entry)) @@ -702,7 +829,7 @@ The future of text editing is here. It's called Neomacs. (profile (cadr entry))) (showcase--schedule delay (lambda () - (neomacs--effects-apply profile) + (showcase--apply-profile profile) (showcase--log " atmosphere: %s" name) (message "Atmosphere: %s" name)))) (setq delay (+ delay 1.0))))) @@ -719,40 +846,38 @@ The future of text editing is here. It's called Neomacs. (let ((features '((0.2 "Line Numbers" (display-line-numbers-mode 1)) (0.8 "Rainbow Indent Guides" - (setopt neomacs-effect-indent-guides - '(:enabled t :rainbow-enabled t))) + (showcase--gallery-set 'indent-guides + '(:enabled t :rainbow-enabled t))) (1.4 "Line Highlight" - (setopt neomacs-effect-line-highlight - '(:enabled t))) + (showcase--gallery-set 'line-highlight '(:enabled t))) (2.0 "Visible Whitespace" - (setopt neomacs-effect-show-whitespace - '(:enabled t :color "#33FF66"))) + (showcase--gallery-set 'show-whitespace + '(:enabled t :color "#33FF66"))) (2.6 "Minimap" - (setopt neomacs-effect-minimap - '(:enabled t))) + (showcase--gallery-set 'minimap '(:enabled t))) (3.2 "Focus Mode" - (setopt neomacs-effect-focus-mode - '(:enabled t :opacity 0.3))) + (showcase--gallery-set 'focus-mode + '(:enabled t :opacity 0.3))) (3.8 "Scroll Progress" - (setopt neomacs-effect-scroll-progress - '(:enabled t))) + (showcase--gallery-set 'scroll-progress '(:enabled t))) (4.5 "Split + Inactive Dimming" (progn (split-window-right) (other-window 1) (switch-to-buffer (showcase--insert-prose-buffer)) (other-window 1) - (setopt neomacs-effect-inactive-dim - '(:enabled t :opacity 0.2)))) + (showcase--gallery-set 'inactive-dim + '(:enabled t :opacity 0.2)))) (5.5 "Zen Mode" (progn (delete-other-windows) (showcase--effect-reset 'focus-mode) (showcase--effect-reset 'minimap) (switch-to-buffer (showcase--insert-prose-buffer)) - (setopt neomacs-effect-zen-mode - '(:enabled t :content-width-pct 60 - :margin-opacity 0.3))))))) + (showcase--gallery-set + 'zen-mode + '(:enabled t :content-width-pct 60 + :margin-opacity 0.3))))))) (dolist (feat features) (let ((d (nth 0 feat)) (name (nth 1 feat)) @@ -1011,9 +1136,10 @@ The future of text editing is here. It's called Neomacs. (lambda () (showcase--log " window-trans: focus border") (message "Transition: Focus Gradient Border") - (setopt neomacs-effect-focus-gradient-border - '(:enabled t :top-color "#4D99FF" - :bot-color "#FF4DFF" :width 3 :opacity 0.8)))) + (showcase--gallery-set + 'focus-gradient-border + '(:enabled t :top-color "#4D99FF" + :bot-color "#FF4DFF" :width 3 :opacity 0.8)))) (showcase--schedule 4.3 (lambda () (other-window 1))) @@ -1259,8 +1385,8 @@ The future of text editing is here. It's called Neomacs. (erase-buffer) (insert (propertize "RETRO MODE ACTIVATED\n" 'face '(:height 2.0 :foreground "#4DFF4D" :weight bold))) (insert "\n"))) - ;; Stack retro effects in one renderer update. - (neomacs--effects-apply + ;; Stack retro effects without rebuilding the other options from defaults. + (showcase--apply-profile '((scanlines :enabled t :spacing 3 :opacity 0.12 :color "#000000") (dot-matrix :enabled t) (bg-pattern :style 1 :spacing 16 :color "#003300" :opacity 0.08) @@ -1367,7 +1493,7 @@ The future of text editing is here. It's called Neomacs. (redisplay t)))))))) ;; Stack the prettiest effects. - (neomacs--effects-apply + (showcase--apply-profile '((aurora :enabled t :color1 "#33CC66" :color2 "#9966FF" :height 100 :opacity 0.2) (neon-border :enabled t) @@ -1453,7 +1579,8 @@ The future of text editing is here. It's called Neomacs. (showcase--log "=== SHOWCASE QUIT ===") (showcase--stop-countdown) (showcase--cancel-seq-timers) - (showcase--reset-all-effects) + (showcase--reset-demo-chrome) + (showcase--end-effect-session) (when showcase--original-mode-line-format (set-default 'mode-line-format showcase--original-mode-line-format) (force-mode-line-update t)) @@ -1507,8 +1634,13 @@ The future of text editing is here. It's called Neomacs. (message "Neomacs Showcase: a=run-all n=next p=prev 0-9=jump q=quit")) (showcase-run-all))) -;; Auto-initialize when loaded -(showcase--init) +(defvar showcase--inhibit-init nil + "When non-nil, loading this file does not start the demo. +Tests bind this before loading so they can call the helpers.") + +;; Auto-initialize when loaded as a demo, not when a test is driving it. +(unless showcase--inhibit-init + (showcase--init)) (provide 'neomacs-showcase) ;;; neomacs-showcase.el ends here From 91d33e57b0e9b7e5dccc06790ba8b3b94ee7a707 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 7 Nov 2026 10:52:07 +0000 Subject: [PATCH 7/8] docs(effects): point configuration at the defcustom interface neomacs-effect-set, neomacs-effect-reset, and neomacs-effects-apply are not defined. The animation page linked from the README, EFFECTS.md, and the display-engine pipeline still called them. Describe setopt, and say that a setq reaches the renderer only when it is made before neomacs-effects.el loads. Co-authored-by: Eval Exec --- .../src/visual_config.rs | 8 +- docs/animations.md | 45 +++++---- docs/design/display-crate-layout.md | 2 +- docs/display-engine/DESIGN.md | 9 +- docs/display-engine/EFFECTS.md | 94 +++++++++++-------- 5 files changed, 92 insertions(+), 66 deletions(-) diff --git a/crates/neomacs-display-protocol/src/visual_config.rs b/crates/neomacs-display-protocol/src/visual_config.rs index 2415e73063..fa2e92f39d 100644 --- a/crates/neomacs-display-protocol/src/visual_config.rs +++ b/crates/neomacs-display-protocol/src/visual_config.rs @@ -93,7 +93,9 @@ impl Default for BufferTransitionConfig { // stock Emacs, the same choice ScrollTransitionConfig makes below. // Opt in at runtime with // (setopt neomacs-buffer-transition-enabled t) - // or through Customize; a plain `setq' never reaches the renderer. + // or through Customize. A plain `setq' after `neomacs-effects.el' + // has loaded never reaches the renderer; a value set before that + // load is applied by the `defcustom'. // The effect/duration/easing below are the values used once it is // enabled. enabled: false, @@ -120,7 +122,9 @@ impl Default for ScrollTransitionConfig { // Off by default: C-v/M-v (and other scrolls) update instantly, // matching stock Emacs. Opt in at runtime with // (setopt neomacs-scroll-transition-enabled t) - // or through Customize; a plain `setq' never reaches the renderer. + // or through Customize. A plain `setq' after `neomacs-effects.el' + // has loaded never reaches the renderer; a value set before that + // load is applied by the `defcustom'. // The effect/duration/easing below are the values used once it is // enabled. enabled: false, diff --git a/docs/animations.md b/docs/animations.md index 14f511ee63..d93a5c76af 100644 --- a/docs/animations.md +++ b/docs/animations.md @@ -90,23 +90,30 @@ orientation, and direction are independent typed settings: ## Configuration ```elisp -;; One typed, name-based profile configures effects and animation policy. -(neomacs-effects-apply - '((cursor-motion :enabled t :speed 15.0 - :style critically-damped-spring :duration 0.15 - :trail-size 0.7) - (buffer-transition :enabled t :duration 0.2 - :effect slide :easing ease-out-quad - :axis auto :direction forward) - (scroll-transition :enabled t :duration 0.15 - :effect page-curl :easing spring))) - -;; Incremental changes use the same names and properties. -(neomacs-effect-set 'cursor-motion - :speed 20.0 :style 'linear :duration 0.1) -(neomacs-effect-set 'buffer-transition - :effect 'page-curl - :axis 'vertical :direction 'backward) -(neomacs-effect-set 'scroll-transition - :effect 'wobbly :easing 'ease-out-quad) +;; Options reach the renderer through their :set function. setopt and +;; Customize do that; a setq after neomacs-effects.el has loaded does not. +(setopt neomacs-cursor-motion-enabled t + neomacs-cursor-motion-speed 15.0 + neomacs-cursor-motion-style 'critically-damped-spring + neomacs-cursor-motion-duration 0.15 + neomacs-cursor-motion-trail-size 0.7 + neomacs-buffer-transition-enabled t + neomacs-buffer-transition-duration 0.2 + neomacs-buffer-transition-effect 'slide + neomacs-buffer-transition-easing 'ease-out-quad + neomacs-buffer-transition-axis 'auto + neomacs-buffer-transition-direction 'forward + neomacs-scroll-transition-enabled t + neomacs-scroll-transition-duration 0.15 + neomacs-scroll-transition-effect 'page-curl + neomacs-scroll-transition-easing 'spring) + +(setopt neomacs-cursor-motion-speed 20.0 + neomacs-cursor-motion-style 'linear + neomacs-cursor-motion-duration 0.1 + neomacs-buffer-transition-effect 'page-curl + neomacs-buffer-transition-axis 'vertical + neomacs-buffer-transition-direction 'backward + neomacs-scroll-transition-effect 'wobbly + neomacs-scroll-transition-easing 'ease-out-quad) ``` diff --git a/docs/design/display-crate-layout.md b/docs/design/display-crate-layout.md index 496e405d6c..5d29809b77 100644 --- a/docs/design/display-crate-layout.md +++ b/docs/design/display-crate-layout.md @@ -131,7 +131,7 @@ Submodule charters — a new file needs one of these to belong: 6. **A config value that Lisp can set must be scalar.** The effect registry is serde reflection over `VisualConfig` and carries only scalars plus `Duration`; a field that serializes to an object breaks - `neomacs-effects-apply` for every effect, not just its own. + `neomacs--effects-apply` for every effect, not just its own. 6. **Lint suppressions are per item, never per crate.** A `#![allow(...)]` in a `lib.rs` covers code nobody has read yet, so the count it hides drifts: `neomacs-layout-engine`'s crate-wide `clippy::too_many_arguments` allow was diff --git a/docs/display-engine/DESIGN.md b/docs/display-engine/DESIGN.md index 8f0dd4e654..3ec591c3df 100644 --- a/docs/display-engine/DESIGN.md +++ b/docs/display-engine/DESIGN.md @@ -289,10 +289,11 @@ region, excluding mode-lines and fringes. ``` Elisp Evaluator / protocol Render thread ───── ──────────────────── ───────────── -(neomacs-effect-set validate named properties ConfigCommand:: - 'cursor-glow against VisualConfig SetVisualConfig(snapshot) - :enabled t apply to cloned snapshot ↓ - :radius 48) commit atomically effect/cursor/transition state +(setopt option :set calls ConfigCommand:: + neomacs-effect- neomacs--effect-set SetVisualConfig(snapshot) + cursor-glow validate named properties ↓ + '(:enabled t against VisualConfig effect/cursor/transition state + :radius 48)) commit atomically ``` Each effect keeps its own typed property schema. Profiles validate completely diff --git a/docs/display-engine/EFFECTS.md b/docs/display-engine/EFFECTS.md index 029e27f89a..052901178c 100644 --- a/docs/display-engine/EFFECTS.md +++ b/docs/display-engine/EFFECTS.md @@ -1,48 +1,62 @@ # Renderer effects -Neomacs exposes renderer effects through one name-based Elisp interface. An -effect has its own typed property set; unrelated effects do not pretend to -share one universal options struct. +Renderer effects are user options. Behavioural effects — cursor motion, +buffer and scroll transitions, window and child-frame animation — are one +option per property. A decorative effect is one option, `neomacs-effect-NAME`, +whose value is a plist of that effect's properties. `setopt` and Customize +are the interface: both run the option's `:set` function, and that is what +reaches the renderer. A plain `setq` after `neomacs-effects.el` has loaded +changes the variable only. A value already set when that file loads is picked +up by the `defcustom`. ```elisp -(neomacs-effect-set 'cursor-glow - :enabled t - :color "#66CCFF" - :radius 48) +(setopt neomacs-effect-cursor-glow + '(:enabled t :color "#66CCFF" :radius 48)) (neomacs-effect-get 'cursor-glow) -(neomacs-effect-reset 'cursor-glow) (neomacs-effect-names) ``` +`neomacs-effect-set`, `neomacs-effect-reset`, and `neomacs-effects-apply` are +not defined. + `neomacs-effect-names` accepts an optional discovery scope: `shader` lists renderer shader configs, `cursor` lists configs valid in a buffer-local cursor profile, and `behavior` lists cursor/window animation policy. With no scope it returns the complete `VisualConfig` registry. -The same interface owns animation behavior that used to be split across +The same options own animation behavior that used to be split across positional setters: ```elisp -(neomacs-effect-set 'cursor-motion - :enabled t :speed 2.4 - :style 'critically-damped-spring - :duration 0.15 :trail-size 0.7) -(neomacs-effect-set 'cursor-color-cycle :enabled t :fps 24) -(neomacs-effect-set 'cursor-blink :enabled t :interval 0.5) -(neomacs-effect-set 'cursor-size-transition :enabled t :duration 0.15) -(neomacs-effect-set 'buffer-transition - :enabled t :duration 0.2 - :effect 'slide :easing 'ease-out-quad - :axis 'auto :direction 'forward) -(neomacs-effect-set 'scroll-transition - :enabled t :duration 0.15 - :effect 'page-curl :easing 'spring) +(setopt neomacs-cursor-motion-enabled t + neomacs-cursor-motion-speed 2.4 + neomacs-cursor-motion-style 'critically-damped-spring + neomacs-cursor-motion-duration 0.15 + neomacs-cursor-motion-trail-size 0.7) +(setopt neomacs-effect-cursor-color-cycle '(:enabled t :fps 24)) +(setopt neomacs-cursor-blink-enabled t + neomacs-cursor-blink-interval 0.5) +(setopt neomacs-cursor-size-transition-enabled t + neomacs-cursor-size-transition-duration 0.15) +(setopt neomacs-buffer-transition-enabled t + neomacs-buffer-transition-duration 0.2 + neomacs-buffer-transition-effect 'slide + neomacs-buffer-transition-easing 'ease-out-quad + neomacs-buffer-transition-axis 'auto + neomacs-buffer-transition-direction 'forward) +(setopt neomacs-scroll-transition-enabled t + neomacs-scroll-transition-duration 0.15 + neomacs-scroll-transition-effect 'page-curl + neomacs-scroll-transition-easing 'spring) ``` -Unknown effects, unknown properties, invalid colors, wrong value types, and -non-integral values for integer properties signal an Elisp error. Property -names use Lisp kebab-case and map to the Rust config field with the same name +`neomacs--effect-set` rejects an unknown effect, an unknown property, an +invalid color, a wrong value type, and a non-integral value for an integer +property. An option's `:set` function stores the Lisp value first and +ignores that signal, so a rejected value changes the option and leaves the +renderer as it was. Property names use Lisp kebab-case and map to the Rust +config field with the same name in snake_case. Colors use sRGB `#RRGGBB` or `#RRGGBBAA` strings and are converted to and from the renderer's linear color storage at the protocol boundary. Time fields whose names end in `-ms` are milliseconds, while Rust @@ -80,25 +94,25 @@ Other motion styles remain available: `exponential`, `critically-damped-spring`, `ease-in-out-cubic`. The plain `ease-out-expo` style moves one rectangle; `neovide` independently animates its corners. -### Applying profiles +### Applying several effects -`neomacs-effects-apply` replaces the complete profile, starting from the -Rust-defined defaults and applying every entry before publishing anything: +Set each option. There is no public command that replaces the whole profile. +`neomacs--effects-apply` is internal: it rebuilds the renderer from the Rust +defaults plus the profile it is given, and it does not write the options, so +calling it directly leaves Customize showing the previous values. An invalid +entry makes that subr signal and publish nothing. ```elisp -(neomacs-effects-apply - '((cursor-glow :enabled t :color "#66CCFF" :radius 48) - (rain-effect :enabled t :drop-count 30 :speed 120.0))) +(setopt neomacs-effect-cursor-glow + '(:enabled t :color "#66CCFF" :radius 48) + neomacs-effect-rain-effect + '(:enabled t :drop-count 30 :speed 120.0)) ``` -If any entry is invalid, neither evaluator state nor renderer state changes. -An empty profile restores every Rust default. `neomacs-effects`, the Custom -option in `term/neo-win.el`, has the same format. - -`neomacs-cursor-effect` accepts one cursor-effect entry or a list of entries -in this format as a buffer-local variable or window parameter. The layout -engine validates it through the same protocol registry used by global -configuration. +`neomacs-cursor-effect`, in `term/neo-win.el`, accepts one +`(EFFECT :PROPERTY VALUE ...)` entry or a list of them as a buffer-local +variable or window parameter. The layout engine validates it through the +same protocol registry used by the options above. ## Ownership and update flow From 1498ec4ddb2fcd7f633e0169543b4be321979f28 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 7 Nov 2026 11:12:44 +0000 Subject: [PATCH 8/8] fix(showcase): show one atmosphere preset at a time showcase--apply-profile merges, so each atmosphere preset left the previous one enabled. Clear the demo slate before a preset, then apply it through the option setters. Require neomacs-effects when the renderer subr exists. Document that a gallery setopt replaces the option plist while the renderer merges, and that cursor blink follows blink-cursor-mode rather than those options. Co-authored-by: Eval Exec --- docs/display-engine/EFFECTS.md | 18 +++++ test/neomacs/neomacs-showcase-effects-test.el | 70 ++++++++++++++++++- test/neomacs/neomacs-showcase.el | 27 ++++++- 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/docs/display-engine/EFFECTS.md b/docs/display-engine/EFFECTS.md index 052901178c..0e8387e126 100644 --- a/docs/display-engine/EFFECTS.md +++ b/docs/display-engine/EFFECTS.md @@ -17,6 +17,13 @@ up by the `defcustom`. (neomacs-effect-names) ``` +A gallery assignment replaces the option's plist. The renderer merges the +properties it is given, so a later `(setopt neomacs-effect-cursor-glow +'(:radius 40))` leaves glow enabled on screen while the option no longer +records `:enabled`. Saving that value and starting again applies only +`:radius` onto the defaults, and the enablement is gone. Include every +property that should persist, or merge onto the current plist first. + `neomacs-effect-set`, `neomacs-effect-reset`, and `neomacs-effects-apply` are not defined. @@ -39,6 +46,17 @@ positional setters: neomacs-cursor-blink-interval 0.5) (setopt neomacs-cursor-size-transition-enabled t neomacs-cursor-size-transition-duration 0.15) +``` + +`neomacs-cursor-blink-enabled` and `neomacs-cursor-blink-interval` are not +what a session keeps. After the first frame, `neomacs--setup-cursor-blink` +copies `blink-cursor-mode` and `blink-cursor-interval` into the renderer +with `neomacs--effect-set`, and a later `blink-cursor-mode` change does +that again. The option variables stay as they were, so a `setopt` of them +in an init file can disappear from the screen. Set `blink-cursor-mode` and +`blink-cursor-interval` for the blink that should persist. + +```elisp (setopt neomacs-buffer-transition-enabled t neomacs-buffer-transition-duration 0.2 neomacs-buffer-transition-effect 'slide diff --git a/test/neomacs/neomacs-showcase-effects-test.el b/test/neomacs/neomacs-showcase-effects-test.el index ea5b1e78e4..850cfca4c0 100644 --- a/test/neomacs/neomacs-showcase-effects-test.el +++ b/test/neomacs/neomacs-showcase-effects-test.el @@ -39,10 +39,10 @@ (list :enabled nil))) (defun neomacs-effect-names (&optional scope) - (let ((shaders '(aurora breathing-border cursor-color-cycle cursor-firework + (let ( (shaders '(aurora breathing-border cursor-color-cycle cursor-firework cursor-glow focus-gradient-border focus-mode - inactive-dim indent-guides line-highlight minimap - scroll-progress show-whitespace zen-mode)) + inactive-dim indent-guides line-highlight matrix-rain + minimap scroll-progress show-whitespace zen-mode)) (behavior '(buffer-transition cursor-blink cursor-motion cursor-size-transition scroll-transition window-animations window-close @@ -216,6 +216,70 @@ (showcase-effects-test-fail "quit left aurora renderer %S" (neomacs-effect-get 'aurora))))) +;; Atmosphere presets are shown one at a time. Applying the next profile +;; must turn the previous preset off in both the option and the renderer. +(let ((aurora 'neomacs-effect-aurora) + (rain 'neomacs-effect-matrix-rain)) + (showcase-effects-test-reset-session) + (showcase-effects-test-custom-clean aurora) + (showcase-effects-test-custom-clean rain) + (showcase-effects-test-user-set aurora '(:enabled nil)) + (showcase--replace-profile '((aurora :enabled t :opacity 0.18))) + (showcase--replace-profile '((matrix-rain :enabled t :opacity 0.15))) + (when (or (eq (showcase-effects-test-enabled (symbol-value aurora)) t) + (eq (showcase-effects-test-enabled (neomacs-effect-get 'aurora)) t)) + (showcase-effects-test-fail + "second preset left aurora option %S renderer %S" + (symbol-value aurora) (neomacs-effect-get 'aurora))) + (unless (and (eq (showcase-effects-test-enabled (symbol-value rain)) t) + (eq (showcase-effects-test-enabled (neomacs-effect-get 'matrix-rain)) t)) + (showcase-effects-test-fail + "second preset desynced matrix-rain option %S renderer %S" + (symbol-value rain) (neomacs-effect-get 'matrix-rain))) + (when (or (get aurora 'customized-value) (get rain 'customized-value)) + (showcase-effects-test-fail "replace-profile recorded a customization")) + (showcase-quit) + (when (eq (showcase-effects-test-enabled (neomacs-effect-get 'matrix-rain)) t) + (showcase-effects-test-fail + "quit left matrix-rain renderer %S" (neomacs-effect-get 'matrix-rain)))) + +;; A gallery :set replaces the option plist and merges into the renderer. +(let* ((sym 'neomacs-effect-cursor-glow) + (setter (get sym 'custom-set))) + (unless setter + (showcase-effects-test-fail "cursor-glow has no :set function")) + (when setter + (funcall setter sym '(:enabled t :color "#112233" :radius 48)) + (funcall setter sym '(:radius 10)) + (let ((option (symbol-value sym)) + (renderer (neomacs-effect-get 'cursor-glow))) + (when (plist-member option :enabled) + (showcase-effects-test-fail + "partial plist kept :enabled in the option %S" option)) + (unless (and (eq (plist-get renderer :enabled) t) + (equal (plist-get renderer :color) "#112233") + (eql (plist-get renderer :radius) 10)) + (showcase-effects-test-fail + "renderer did not keep omitted glow properties %S" renderer))))) + +;; The effects library is required when the renderer subr exists, and the +;; load stays quiet when that subr is absent. +(cl-letf (((symbol-function 'require) (lambda (&rest _) nil))) + (condition-case err + (progn + (showcase--require-effects) + (showcase-effects-test-fail "missing neomacs-effects did not signal")) + (error + (unless (string-match-p "neomacs-effects" (error-message-string err)) + (showcase-effects-test-fail "unexpected require error %S" err))))) +(let ((orig (symbol-function 'neomacs--effect-set))) + (fmakunbound 'neomacs--effect-set) + (condition-case err + (showcase--require-effects) + (error + (showcase-effects-test-fail "require signaled without the subr: %S" err))) + (fset 'neomacs--effect-set orig)) + (when showcase-effects-test-failures (princ (format "FAILED %d\n" (length showcase-effects-test-failures))) (kill-emacs 1)) diff --git a/test/neomacs/neomacs-showcase.el b/test/neomacs/neomacs-showcase.el index 94dcabcc92..3515671dba 100644 --- a/test/neomacs/neomacs-showcase.el +++ b/test/neomacs/neomacs-showcase.el @@ -4,7 +4,19 @@ (require 'neomacs-webkit nil t) (require 'neomacs-video nil t) (require 'neomacs-image nil t) -(require 'neomacs-effects nil t) + +(defun showcase--require-effects () + "Load `neomacs-effects' when the renderer can be configured. +WebKit, video, and image are optional. The effect options are not: +without them a later setter only changes a variable and the renderer +never sees it. When the renderer subr is absent, keep the load +optional so this file can still be read." + (if (fboundp 'neomacs--effect-set) + (unless (require 'neomacs-effects nil t) + (error "Showcase needs library `neomacs-effects'")) + (require 'neomacs-effects nil t))) + +(showcase--require-effects) (defvar showcase--log-file "/tmp/neomacs-showcase.log" "Path to the log file.") @@ -408,10 +420,19 @@ merged plist. Otherwise only the renderer changes." "Apply each entry of PROFILE with `showcase--gallery-set'. PROFILE has the same shape `neomacs--effects-apply' accepts. Unlike that subr, this does not rebuild every effect from the Rust defaults, -so options the profile does not mention stay in sync with the renderer." +so options the profile does not mention stay in sync with the renderer. +Call `showcase--replace-profile' when the profile should be shown alone." (dolist (entry profile) (showcase--gallery-set (car entry) (cdr entry)))) +(defun showcase--replace-profile (profile) + "Show PROFILE alone on the demo slate. +Clears every effect to the demo defaults, then applies PROFILE through +the option setters. The user's pre-demo values stay in the snapshot +`showcase-quit' restores." + (showcase--demo-clean-effects) + (showcase--apply-profile profile)) + (defun showcase--effect-enable (effect) "Turn on gallery EFFECT without recording a user customization." (showcase--gallery-set effect '(:enabled t))) @@ -829,7 +850,7 @@ back by `showcase-quit'. Nothing here is recorded as a customization." (profile (cadr entry))) (showcase--schedule delay (lambda () - (showcase--apply-profile profile) + (showcase--replace-profile profile) (showcase--log " atmosphere: %s" name) (message "Atmosphere: %s" name)))) (setq delay (+ delay 1.0)))))