From 6b761f83f07c176a58857086003cf4bd05d77721 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:14:56 +0200 Subject: [PATCH 01/18] Add disabled state hook to elements --- .../main/java/com/spinyowl/spinygui/core/node/Element.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/Element.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/Element.java index b4162f11..38774c2c 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/Element.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/Element.java @@ -114,6 +114,11 @@ public boolean hasAttribute(String attribute) { return attributes.containsKey(attribute); } + /** Returns whether this element is a control disabled by its {@code disabled} attribute. */ + public boolean disabled() { + return false; + } + @Override public void removeAttribute(String attribute) { attributes.remove(attribute); From 64ec8e5f35348959241cad0424aae30472ead98b Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:15:07 +0200 Subject: [PATCH 02/18] Honor disabled attribute on buttons --- .../com/spinyowl/spinygui/core/node/ButtonElement.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/ButtonElement.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/ButtonElement.java index a240fe13..d9779d98 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/ButtonElement.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/ButtonElement.java @@ -1,5 +1,6 @@ package com.spinyowl.spinygui.core.node; +import static com.spinyowl.spinygui.core.node.NodeBuilder.ATTR_DISABLED; import static com.spinyowl.spinygui.core.node.NodeBuilder.ATTR_TYPE; import static com.spinyowl.spinygui.core.node.NodeBuilder.NODE_BUTTON; import static com.spinyowl.spinygui.core.node.NodeBuilder.TYPE_BUTTON; @@ -37,6 +38,11 @@ public void type(String type) { this.type = type == null || type.isBlank() ? TYPE_SUBMIT : type; } + @Override + public boolean disabled() { + return hasAttribute(ATTR_DISABLED); + } + public boolean plainButton() { return TYPE_BUTTON.equalsIgnoreCase(type); } @@ -50,6 +56,6 @@ public boolean resetButton() { } public boolean activatable() { - return plainButton() || submitButton() || resetButton(); + return !disabled() && (plainButton() || submitButton() || resetButton()); } } From 1f67bd02de38c10f94930198fe3ed22a9851dd08 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:15:40 +0200 Subject: [PATCH 03/18] Add disabled attribute builder --- .../java/com/spinyowl/spinygui/core/node/NodeBuilder.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/NodeBuilder.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/NodeBuilder.java index b22a1e2b..4ef8a399 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/NodeBuilder.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/NodeBuilder.java @@ -11,6 +11,7 @@ public final class NodeBuilder { public static final String ATTR_TYPE = "type"; public static final String ATTR_NAME = "name"; public static final String ATTR_VALUE = "value"; + public static final String ATTR_DISABLED = "disabled"; public static final String ATTR_ROWS = "rows"; public static final String ATTR_COLS = "cols"; @@ -99,6 +100,11 @@ public static Attribute value(String value) { return attr(ATTR_VALUE, value); } + /** Creates a boolean {@code disabled} attribute. */ + public static Attribute disabled() { + return attr(ATTR_DISABLED, ""); + } + public static Attribute rows(String value) { return attr(ATTR_ROWS, value); } From 545af423cbd59b66b1b58c64160665302d140b75 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:15:56 +0200 Subject: [PATCH 04/18] Honor disabled attribute on inputs --- .../java/com/spinyowl/spinygui/core/node/InputElement.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/InputElement.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/InputElement.java index 6cd87b24..0e268946 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/InputElement.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/InputElement.java @@ -1,5 +1,6 @@ package com.spinyowl.spinygui.core.node; +import static com.spinyowl.spinygui.core.node.NodeBuilder.ATTR_DISABLED; import static com.spinyowl.spinygui.core.node.NodeBuilder.ATTR_TYPE; import static com.spinyowl.spinygui.core.node.NodeBuilder.ATTR_VALUE; import static com.spinyowl.spinygui.core.node.NodeBuilder.NODE_INPUT; @@ -115,6 +116,11 @@ public void textScrollLeft(float textScrollLeft) { this.textScrollLeft = Math.max(0, textScrollLeft); } + @Override + public boolean disabled() { + return hasAttribute(ATTR_DISABLED); + } + public boolean textInput() { return TYPE_TEXT.equalsIgnoreCase(type); } From 1f30c89b40f072d956cfd21c649ef3dc7e699fb7 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:16:12 +0200 Subject: [PATCH 05/18] Honor disabled attribute on textareas --- .../com/spinyowl/spinygui/core/node/TextareaElement.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/TextareaElement.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/TextareaElement.java index 2ea3b130..5830770d 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/TextareaElement.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/node/TextareaElement.java @@ -1,5 +1,6 @@ package com.spinyowl.spinygui.core.node; +import static com.spinyowl.spinygui.core.node.NodeBuilder.ATTR_DISABLED; import static com.spinyowl.spinygui.core.node.NodeBuilder.NODE_TEXTAREA; import java.util.Map; @@ -105,6 +106,11 @@ public void textScrollLeft(float textScrollLeft) { this.textScrollLeft = Math.max(0, textScrollLeft); } + @Override + public boolean disabled() { + return hasAttribute(ATTR_DISABLED); + } + private int clampTextIndex(int index) { return TextIndexNormalizer.clampAndSnapBackward(value, index); } From 3ee5b443d78e5ef00d484f0e40891ab82280ae15 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:16:21 +0200 Subject: [PATCH 06/18] Block disabled button activation --- .../com/spinyowl/spinygui/core/system/input/ButtonBehavior.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/input/ButtonBehavior.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/input/ButtonBehavior.java index 9b035f2a..b7713699 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/input/ButtonBehavior.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/input/ButtonBehavior.java @@ -14,7 +14,7 @@ public boolean handleKey(ButtonElement button, KeyCode keyCode, KeyAction action } public boolean handleKey(InputElement input, KeyCode keyCode, KeyAction action) { - return handleKey(input, input.buttonInput(), keyCode, action); + return handleKey(input, input.buttonInput() && !input.disabled(), keyCode, action); } private boolean handleKey( From 6389cb3b68b6295c731e426054570e610be8f05a Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:16:37 +0200 Subject: [PATCH 07/18] Ignore char input for disabled controls --- .../core/system/event/listener/SystemCharEventListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemCharEventListener.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemCharEventListener.java index 71006ab3..1f6e8ae0 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemCharEventListener.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemCharEventListener.java @@ -62,7 +62,7 @@ public void processWithImpact( private void processInternal( SystemCharEvent event, Frame frame, InputProcessingBatch batch) { var focusedElement = frame.getFocusedElement(); - if (focusedElement == null) { + if (focusedElement == null || focusedElement.disabled()) { return; } From ba5ded811531dea01873c23ccd64fe8a745ea212 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:17:06 +0200 Subject: [PATCH 08/18] Ignore key input for disabled controls --- .../core/system/event/listener/SystemKeyEventListener.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemKeyEventListener.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemKeyEventListener.java index a416993a..30770d73 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemKeyEventListener.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemKeyEventListener.java @@ -80,6 +80,9 @@ public void processWithImpact( private void processInternal( SystemKeyEvent event, Frame frame, InputProcessingBatch batch) { var element = frame.getFocusedElement(); + if (element != null && element.disabled()) { + return; + } if (element == null && batch == null) { return; } From bdd1c233ead85de7dfb38190756cdd0da771e420 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:17:36 +0200 Subject: [PATCH 09/18] Ignore mouse input for disabled controls --- .../event/listener/SystemMouseClickEventListener.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemMouseClickEventListener.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemMouseClickEventListener.java index 1b12eded..c4e3637a 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemMouseClickEventListener.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemMouseClickEventListener.java @@ -90,6 +90,10 @@ public void process(@NonNull SystemMouseClickEvent event, @NonNull Frame frame) processWithNoTarget(event, frame, focusedElement, currentCursorPosition); } else { target = buttonOwner(target); + if (target.disabled()) { + target.pressed(false); + return; + } processWithExistingTarget(event, frame, focusedElement, currentCursorPosition, target); } } @@ -291,7 +295,7 @@ private void generateActionEvent(Frame frame, Element target) { private boolean activatable(Element target) { return target instanceof ButtonElement button && button.activatable() - || target instanceof InputElement input && input.buttonInput(); + || target instanceof InputElement input && input.buttonInput() && !input.disabled(); } private Element buttonOwner(Element target) { From 041bdd121e64e20a0339d2a6c246d14901839389 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:17:46 +0200 Subject: [PATCH 10/18] Add disabled pseudo class selector --- .../selector/pseudoclass/DisabledSelector.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 spinygui.core/src/main/java/com/spinyowl/spinygui/core/style/stylesheet/selector/pseudoclass/DisabledSelector.java diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/style/stylesheet/selector/pseudoclass/DisabledSelector.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/style/stylesheet/selector/pseudoclass/DisabledSelector.java new file mode 100644 index 00000000..68d4e2da --- /dev/null +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/style/stylesheet/selector/pseudoclass/DisabledSelector.java @@ -0,0 +1,18 @@ +package com.spinyowl.spinygui.core.style.stylesheet.selector.pseudoclass; + +import com.spinyowl.spinygui.core.node.Element; +import com.spinyowl.spinygui.core.style.stylesheet.selector.PseudoClassSelector; + +/** Matches supported form controls that currently have the boolean {@code disabled} attribute. */ +public class DisabledSelector implements PseudoClassSelector { + + @Override + public boolean test(Element element) { + return element.disabled(); + } + + @Override + public String toString() { + return ":disabled"; + } +} From 8e139599865b871ae86367a209da0feba7e5c9e9 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:18:06 +0200 Subject: [PATCH 11/18] Parse disabled pseudo class --- .../core/parser/impl/css/visitor/SelectorVisitor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/parser/impl/css/visitor/SelectorVisitor.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/parser/impl/css/visitor/SelectorVisitor.java index 62785bf5..ee071219 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/parser/impl/css/visitor/SelectorVisitor.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/parser/impl/css/visitor/SelectorVisitor.java @@ -14,6 +14,7 @@ import com.spinyowl.spinygui.core.style.stylesheet.selector.combinator.DescendantSelector; import com.spinyowl.spinygui.core.style.stylesheet.selector.combinator.GeneralSiblingSelector; import com.spinyowl.spinygui.core.style.stylesheet.selector.pseudoclass.ActiveSelector; +import com.spinyowl.spinygui.core.style.stylesheet.selector.pseudoclass.DisabledSelector; import com.spinyowl.spinygui.core.style.stylesheet.selector.pseudoclass.FocusSelector; import com.spinyowl.spinygui.core.style.stylesheet.selector.pseudoclass.HoverSelector; import com.spinyowl.spinygui.core.style.stylesheet.selector.pseudoelement.AfterSelector; @@ -99,6 +100,10 @@ public Selector visitPseudo(PseudoContext ctx) { return new ActiveSelector(); } + if ("disabled".equals(selectorName)) { + return new DisabledSelector(); + } + if ("before".equals(selectorName)) { return new BeforeSelector(); } From df831894b7ee1550b7641b6f976d518029e7e733 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:18:45 +0200 Subject: [PATCH 12/18] Test disabled attribute state --- .../core/node/DisabledAttributeTest.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 spinygui.core/src/test/java/com/spinyowl/spinygui/core/node/DisabledAttributeTest.java diff --git a/spinygui.core/src/test/java/com/spinyowl/spinygui/core/node/DisabledAttributeTest.java b/spinygui.core/src/test/java/com/spinyowl/spinygui/core/node/DisabledAttributeTest.java new file mode 100644 index 00000000..af9308b6 --- /dev/null +++ b/spinygui.core/src/test/java/com/spinyowl/spinygui/core/node/DisabledAttributeTest.java @@ -0,0 +1,57 @@ +package com.spinyowl.spinygui.core.node; + +import static com.spinyowl.spinygui.core.node.NodeBuilder.ATTR_DISABLED; +import static com.spinyowl.spinygui.core.node.NodeBuilder.TYPE_BUTTON; +import static com.spinyowl.spinygui.core.node.NodeBuilder.attrs; +import static com.spinyowl.spinygui.core.node.NodeBuilder.button; +import static com.spinyowl.spinygui.core.node.NodeBuilder.disabled; +import static com.spinyowl.spinygui.core.node.NodeBuilder.div; +import static com.spinyowl.spinygui.core.node.NodeBuilder.input; +import static com.spinyowl.spinygui.core.node.NodeBuilder.textarea; +import static com.spinyowl.spinygui.core.node.NodeBuilder.type; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class DisabledAttributeTest { + + @Test + void disabledAttributeMarksSupportedControlsDisabled() { + ButtonElement button = button(attrs(disabled())); + InputElement input = input(attrs(disabled())); + TextareaElement textarea = textarea(attrs(disabled()), "value"); + + assertTrue(button.disabled()); + assertTrue(input.disabled()); + assertTrue(textarea.disabled()); + } + + @Test + void disabledAttributeUsesBooleanPresenceSemantics() { + InputElement input = input(); + + input.setAttribute(ATTR_DISABLED, "false"); + assertTrue(input.disabled()); + + input.removeAttribute(ATTR_DISABLED); + assertFalse(input.disabled()); + } + + @Test + void unsupportedElementDoesNotBecomeDisabled() { + Element element = div(attrs(disabled())); + + assertFalse(element.disabled()); + } + + @Test + void disabledButtonIsNotActivatable() { + ButtonElement button = button(attrs(disabled(), type(TYPE_BUTTON))); + + assertFalse(button.activatable()); + + button.removeAttribute(ATTR_DISABLED); + assertTrue(button.activatable()); + } +} From db608820e240f80a22751362a43e2cd8cc2e299c Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:18:53 +0200 Subject: [PATCH 13/18] Test disabled pseudo class selector --- .../pseudoclass/DisabledSelectorTest.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 spinygui.core/src/test/java/com/spinyowl/spinygui/core/style/stylesheet/selector/pseudoclass/DisabledSelectorTest.java diff --git a/spinygui.core/src/test/java/com/spinyowl/spinygui/core/style/stylesheet/selector/pseudoclass/DisabledSelectorTest.java b/spinygui.core/src/test/java/com/spinyowl/spinygui/core/style/stylesheet/selector/pseudoclass/DisabledSelectorTest.java new file mode 100644 index 00000000..3b975d47 --- /dev/null +++ b/spinygui.core/src/test/java/com/spinyowl/spinygui/core/style/stylesheet/selector/pseudoclass/DisabledSelectorTest.java @@ -0,0 +1,32 @@ +package com.spinyowl.spinygui.core.style.stylesheet.selector.pseudoclass; + +import static com.spinyowl.spinygui.core.node.NodeBuilder.attrs; +import static com.spinyowl.spinygui.core.node.NodeBuilder.button; +import static com.spinyowl.spinygui.core.node.NodeBuilder.disabled; +import static com.spinyowl.spinygui.core.node.NodeBuilder.div; +import static com.spinyowl.spinygui.core.node.NodeBuilder.input; +import static com.spinyowl.spinygui.core.node.NodeBuilder.textarea; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class DisabledSelectorTest { + + private final DisabledSelector selector = new DisabledSelector(); + + @Test + void matchesDisabledSupportedControls() { + assertTrue(selector.test(button(attrs(disabled())))); + assertTrue(selector.test(input(attrs(disabled())))); + assertTrue(selector.test(textarea(attrs(disabled()), "value"))); + } + + @Test + void doesNotMatchEnabledOrUnsupportedElements() { + assertFalse(selector.test(button())); + assertFalse(selector.test(input())); + assertFalse(selector.test(textarea())); + assertFalse(selector.test(div(attrs(disabled())))); + } +} From 04abab7dcab06632e037b17f7ed300431cbd2db2 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:19:20 +0200 Subject: [PATCH 14/18] Test disabled control event suppression --- .../DisabledControlEventListenerTest.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 spinygui.core/src/test/java/com/spinyowl/spinygui/core/system/event/listener/DisabledControlEventListenerTest.java diff --git a/spinygui.core/src/test/java/com/spinyowl/spinygui/core/system/event/listener/DisabledControlEventListenerTest.java b/spinygui.core/src/test/java/com/spinyowl/spinygui/core/system/event/listener/DisabledControlEventListenerTest.java new file mode 100644 index 00000000..27da6b37 --- /dev/null +++ b/spinygui.core/src/test/java/com/spinyowl/spinygui/core/system/event/listener/DisabledControlEventListenerTest.java @@ -0,0 +1,126 @@ +package com.spinyowl.spinygui.core.system.event.listener; + +import static com.spinyowl.spinygui.core.node.NodeBuilder.attrs; +import static com.spinyowl.spinygui.core.node.NodeBuilder.button; +import static com.spinyowl.spinygui.core.node.NodeBuilder.disabled; +import static com.spinyowl.spinygui.core.node.NodeBuilder.frame; +import static com.spinyowl.spinygui.core.node.NodeBuilder.textarea; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import com.spinyowl.spinygui.core.event.processor.EventProcessor; +import com.spinyowl.spinygui.core.input.Keyboard; +import com.spinyowl.spinygui.core.input.MouseService; +import com.spinyowl.spinygui.core.input.MouseService.CursorPositions; +import com.spinyowl.spinygui.core.node.ButtonElement; +import com.spinyowl.spinygui.core.node.Frame; +import com.spinyowl.spinygui.core.node.TextareaElement; +import com.spinyowl.spinygui.core.system.event.SystemCharEvent; +import com.spinyowl.spinygui.core.system.event.SystemKeyEvent; +import com.spinyowl.spinygui.core.system.event.SystemMouseClickEvent; +import com.spinyowl.spinygui.core.system.input.SystemKeyAction; +import com.spinyowl.spinygui.core.system.input.SystemMouseButton; +import com.spinyowl.spinygui.core.time.TimeService; +import org.joml.Vector2f; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DisabledControlEventListenerTest { + + @Mock private EventProcessor eventProcessor; + @Mock private TimeService timeService; + @Mock private MouseService mouseService; + @Mock private Keyboard keyboard; + + @Test + void mousePressOnDisabledButtonDoesNotFocusPressOrDispatchEvents() { + ButtonElement button = button(attrs(disabled())); + button.box().contentSize(20, 20); + button.box().contentPosition(20, 20); + Frame frame = frame(button); + frame.box().contentSize(100, 100); + + Vector2f cursor = new Vector2f(25, 25); + when(mouseService.getCursorPositions(frame)).thenReturn(new CursorPositions(cursor, cursor)); + + SystemMouseClickEvent event = + SystemMouseClickEvent.builder() + .action(SystemKeyAction.PRESS) + .mods(ImmutableSet.of()) + .frame(frame) + .button(SystemMouseButton.LEFT) + .build(); + + var listener = + SystemMouseClickEventListener.builder() + .eventProcessor(eventProcessor) + .timeService(timeService) + .mouseService(mouseService) + .build(); + + listener.process(event, frame); + + verify(mouseService).pressed(event.button().mouseButton(), true); + verify(mouseService).getCursorPositions(frame); + verifyNoInteractions(eventProcessor, timeService); + assertFalse(button.focused()); + assertFalse(button.pressed()); + } + + @Test + void keyPressOnFocusedDisabledButtonDoesNotActivateOrDispatchKeyboardEvent() { + ButtonElement button = button(attrs(disabled())); + button.focused(true); + Frame frame = frame(button); + + SystemKeyEvent event = + SystemKeyEvent.builder() + .keyCode(13) + .scancode(13) + .action(SystemKeyAction.PRESS) + .mods(ImmutableSet.of()) + .frame(frame) + .build(); + + var listener = + SystemKeyEventListener.builder() + .eventProcessor(eventProcessor) + .timeService(timeService) + .keyboard(keyboard) + .build(); + + listener.process(event, frame); + + verifyNoInteractions(eventProcessor, timeService, keyboard); + assertFalse(button.pressed()); + } + + @Test + void charInputOnFocusedDisabledTextareaDoesNotEditOrDispatchCharEvent() { + TextareaElement textarea = textarea(attrs(disabled()), "ac"); + textarea.caretIndex(1); + textarea.focused(true); + Frame frame = frame(textarea); + + SystemCharEvent event = SystemCharEvent.builder().frame(frame).codepoint('b').build(); + + var listener = + SystemCharEventListener.builder() + .eventProcessor(eventProcessor) + .timeService(timeService) + .build(); + + listener.process(event, frame); + + assertEquals("ac", textarea.value()); + assertEquals(1, textarea.caretIndex()); + verifyNoInteractions(eventProcessor, timeService); + } +} From f98c779efcd6be1aa7359583f1ea2b689dad84d6 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:24:01 +0200 Subject: [PATCH 15/18] Clear pressed state for disabled key targets --- .../core/system/event/listener/SystemKeyEventListener.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemKeyEventListener.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemKeyEventListener.java index 30770d73..bf8e5582 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemKeyEventListener.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemKeyEventListener.java @@ -81,6 +81,7 @@ private void processInternal( SystemKeyEvent event, Frame frame, InputProcessingBatch batch) { var element = frame.getFocusedElement(); if (element != null && element.disabled()) { + element.pressed(false); return; } if (element == null && batch == null) { From eb529c963559c2bf1cf79d8017108e52eefc4197 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:24:12 +0200 Subject: [PATCH 16/18] Clear disabled button pressed state --- .../spinyowl/spinygui/core/system/input/ButtonBehavior.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/input/ButtonBehavior.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/input/ButtonBehavior.java index b7713699..7483d017 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/input/ButtonBehavior.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/input/ButtonBehavior.java @@ -19,7 +19,11 @@ public boolean handleKey(InputElement input, KeyCode keyCode, KeyAction action) private boolean handleKey( Element element, boolean activatable, KeyCode keyCode, KeyAction action) { - if (!activatable || !activationKey(keyCode)) { + if (!activatable) { + element.pressed(false); + return false; + } + if (!activationKey(keyCode)) { return false; } From ce46a88d5d3310d16769cec04b946f8617aa986c Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:25:46 +0200 Subject: [PATCH 17/18] Preserve release cleanup over disabled targets --- .../event/listener/SystemMouseClickEventListener.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemMouseClickEventListener.java b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemMouseClickEventListener.java index c4e3637a..653558a5 100644 --- a/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemMouseClickEventListener.java +++ b/spinygui.core/src/main/java/com/spinyowl/spinygui/core/system/event/listener/SystemMouseClickEventListener.java @@ -92,6 +92,12 @@ public void process(@NonNull SystemMouseClickEvent event, @NonNull Frame frame) target = buttonOwner(target); if (target.disabled()) { target.pressed(false); + if (event.action() == RELEASE && focusedElement != null && focusedElement != target) { + focusedElement.pressed(false); + if (!focusedElement.disabled()) { + generateReleaseEvent(event, frame, focusedElement, currentCursorPosition); + } + } return; } processWithExistingTarget(event, frame, focusedElement, currentCursorPosition, target); From 12d523485e14b385fbace7edb605a4532e512356 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Wed, 19 Aug 2026 22:26:14 +0200 Subject: [PATCH 18/18] Cover disabled release state cleanup --- .../DisabledControlEventListenerTest.java | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/spinygui.core/src/test/java/com/spinyowl/spinygui/core/system/event/listener/DisabledControlEventListenerTest.java b/spinygui.core/src/test/java/com/spinyowl/spinygui/core/system/event/listener/DisabledControlEventListenerTest.java index 27da6b37..5726a9e6 100644 --- a/spinygui.core/src/test/java/com/spinyowl/spinygui/core/system/event/listener/DisabledControlEventListenerTest.java +++ b/spinygui.core/src/test/java/com/spinyowl/spinygui/core/system/event/listener/DisabledControlEventListenerTest.java @@ -5,13 +5,15 @@ import static com.spinyowl.spinygui.core.node.NodeBuilder.disabled; import static com.spinyowl.spinygui.core.node.NodeBuilder.frame; import static com.spinyowl.spinygui.core.node.NodeBuilder.textarea; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableSet; +import com.spinyowl.spinygui.core.event.MouseClickEvent; import com.spinyowl.spinygui.core.event.processor.EventProcessor; import com.spinyowl.spinygui.core.input.Keyboard; import com.spinyowl.spinygui.core.input.MouseService; @@ -74,10 +76,52 @@ void mousePressOnDisabledButtonDoesNotFocusPressOrDispatchEvents() { assertFalse(button.pressed()); } + @Test + void mouseReleaseOverDisabledButtonClearsPreviouslyPressedControl() { + ButtonElement disabledButton = button(attrs(disabled())); + disabledButton.box().contentSize(20, 20); + disabledButton.box().contentPosition(20, 20); + + ButtonElement focusedButton = button(); + focusedButton.focused(true); + focusedButton.pressed(true); + focusedButton.box().contentSize(20, 20); + focusedButton.box().contentPosition(50, 20); + + Frame frame = frame(disabledButton, focusedButton); + frame.box().contentSize(100, 100); + + Vector2f cursor = new Vector2f(25, 25); + when(mouseService.getCursorPositions(frame)).thenReturn(new CursorPositions(cursor, cursor)); + when(timeService.currentTime()).thenReturn(1D); + + SystemMouseClickEvent event = + SystemMouseClickEvent.builder() + .action(SystemKeyAction.RELEASE) + .mods(ImmutableSet.of()) + .frame(frame) + .button(SystemMouseButton.LEFT) + .build(); + + var listener = + SystemMouseClickEventListener.builder() + .eventProcessor(eventProcessor) + .timeService(timeService) + .mouseService(mouseService) + .build(); + + listener.process(event, frame); + + assertFalse(disabledButton.pressed()); + assertFalse(focusedButton.pressed()); + verify(eventProcessor).push(any(MouseClickEvent.class)); + } + @Test void keyPressOnFocusedDisabledButtonDoesNotActivateOrDispatchKeyboardEvent() { ButtonElement button = button(attrs(disabled())); button.focused(true); + button.pressed(true); Frame frame = frame(button); SystemKeyEvent event =