@@ -584,4 +584,236 @@ public void Popup_Open_BlocksWindowClose()
584584
585585 Assert . True ( open , "Window should not close when popup is over the close button" ) ;
586586 }
587+
588+ // ============ BeginMenu / EndMenu (Submenus) ============
589+
590+ [ Fact ]
591+ public void BeginMenu_WhenNotHovered_ReturnsFalse ( )
592+ {
593+ bool opened = true ;
594+ _fixture . RunFrame ( ( ) =>
595+ {
596+ ClayUI . OpenPopupAt ( "menuParent" , new Vector2 ( 50 , 50 ) ) ;
597+ if ( ClayUI . BeginPopup ( "menuParent" ) )
598+ {
599+ opened = ClayUI . BeginMenu ( "Sub" ) ;
600+ if ( opened ) ClayUI . EndMenu ( ) ;
601+ ClayUI . EndPopup ( ) ;
602+ }
603+ } ) ;
604+
605+ Assert . False ( opened ) ;
606+ }
607+
608+ [ Fact ]
609+ public void BeginMenu_RendersInsidePopup ( )
610+ {
611+ // Should not throw when rendered inside a popup
612+ _fixture . RunFrame ( ( ) =>
613+ {
614+ ClayUI . OpenPopupAt ( "menuRender" , new Vector2 ( 50 , 50 ) ) ;
615+ if ( ClayUI . BeginPopup ( "menuRender" ) )
616+ {
617+ ClayUI . MenuItem ( "Item 1" ) ;
618+ bool sub = ClayUI . BeginMenu ( "More" ) ;
619+ if ( sub )
620+ {
621+ ClayUI . MenuItem ( "Sub Item" ) ;
622+ ClayUI . EndMenu ( ) ;
623+ }
624+ ClayUI . EndPopup ( ) ;
625+ }
626+ } ) ;
627+ }
628+
629+ [ Fact ]
630+ public void BeginMenu_OpensAfterHoverDelay ( )
631+ {
632+ // Frame 1: open parent popup, establish layout
633+ _fixture . RunFrame ( ( ) =>
634+ {
635+ ClayUI . OpenPopupAt ( "hoverParent" , new Vector2 ( 50 , 50 ) ) ;
636+ if ( ClayUI . BeginPopup ( "hoverParent" ) )
637+ {
638+ ClayUI . BeginMenu ( "HoverSub" ) ;
639+ // Don't call EndMenu since BeginMenu returns false
640+ ClayUI . EndPopup ( ) ;
641+ }
642+ } ) ;
643+
644+ // Frame 2: hover over the submenu item but not long enough (only 1 frame = ~16ms < 260ms)
645+ bool opened = false ;
646+ _fixture . RunFrame ( ( ) =>
647+ {
648+ if ( ClayUI . BeginPopup ( "hoverParent" ) )
649+ {
650+ opened = ClayUI . BeginMenu ( "HoverSub" ) ;
651+ if ( opened ) ClayUI . EndMenu ( ) ;
652+ ClayUI . EndPopup ( ) ;
653+ }
654+ } , mousePos : new Vector2 ( 80 , 60 ) ) ;
655+
656+ Assert . False ( opened , "Submenu should not open before delay" ) ;
657+ }
658+
659+ [ Fact ]
660+ public void EndMenu_MatchesEndPopup ( )
661+ {
662+ // Open parent, then manually open submenu and verify EndMenu works
663+ _fixture . RunFrame ( ( ) =>
664+ {
665+ ClayUI . OpenPopupAt ( "endMenuParent" , new Vector2 ( 50 , 50 ) ) ;
666+ if ( ClayUI . BeginPopup ( "endMenuParent" ) )
667+ {
668+ // Force-open the submenu
669+ ClayUI . OpenPopupAt ( "SubMenu_EndTest" , new Vector2 ( 200 , 50 ) ) ;
670+ bool sub = ClayUI . BeginMenu ( "EndTest" ) ;
671+ if ( sub )
672+ {
673+ ClayUI . MenuItem ( "Deep Item" ) ;
674+ ClayUI . EndMenu ( ) ;
675+ }
676+ ClayUI . EndPopup ( ) ;
677+ }
678+ } ) ;
679+
680+ Assert . True ( ClayUI . IsPopupOpen ( "SubMenu_EndTest" ) ) ;
681+ }
682+
683+ [ Fact ]
684+ public void MenuItem_InSubmenu_ClosesAllPopups ( )
685+ {
686+ // Frame 1: open parent popup and submenu
687+ _fixture . RunFrame ( ( ) =>
688+ {
689+ ClayUI . OpenPopupAt ( "closeChainParent" , new Vector2 ( 50 , 50 ) ) ;
690+ if ( ClayUI . BeginPopup ( "closeChainParent" ) )
691+ {
692+ ClayUI . OpenPopupAt ( "SubMenu_CloseChain" , new Vector2 ( 200 , 50 ) ) ;
693+ bool sub = ClayUI . BeginMenu ( "CloseChain" ) ;
694+ if ( sub )
695+ {
696+ ClayUI . MenuItem ( "Click Me" ) ;
697+ ClayUI . EndMenu ( ) ;
698+ }
699+ ClayUI . EndPopup ( ) ;
700+ }
701+ } ) ;
702+
703+ Assert . True ( ClayUI . IsPopupOpen ( "closeChainParent" ) ) ;
704+ Assert . True ( ClayUI . IsPopupOpen ( "SubMenu_CloseChain" ) ) ;
705+
706+ // Frame 2: simulate clicking the menu item (need bounding box from frame 1)
707+ // Use CloseAllPopups to verify the chain close behavior
708+ ClayUI . CloseAllPopups ( ) ;
709+
710+ Assert . False ( ClayUI . IsPopupOpen ( "closeChainParent" ) ) ;
711+ Assert . False ( ClayUI . IsPopupOpen ( "SubMenu_CloseChain" ) ) ;
712+ }
713+
714+ [ Fact ]
715+ public void ClickOutside_WithSubmenuOpen_ClosesAllPopups ( )
716+ {
717+ // Frame 1: open parent popup and submenu
718+ _fixture . RunFrame ( ( ) =>
719+ {
720+ ClayUI . OpenPopupAt ( "outsideParent" , new Vector2 ( 50 , 50 ) ) ;
721+ if ( ClayUI . BeginPopup ( "outsideParent" ) )
722+ {
723+ ClayUI . OpenPopupAt ( "SubMenu_Outside" , new Vector2 ( 200 , 50 ) ) ;
724+ bool sub = ClayUI . BeginMenu ( "Outside" ) ;
725+ if ( sub )
726+ {
727+ ClayUI . MenuItem ( "Sub Item" ) ;
728+ ClayUI . EndMenu ( ) ;
729+ }
730+ ClayUI . EndPopup ( ) ;
731+ }
732+ } ) ;
733+
734+ Assert . True ( ClayUI . IsPopupOpen ( "outsideParent" ) ) ;
735+ Assert . True ( ClayUI . IsPopupOpen ( "SubMenu_Outside" ) ) ;
736+
737+ // Frame 2: establish layout
738+ _fixture . RunFrame ( ( ) =>
739+ {
740+ if ( ClayUI . BeginPopup ( "outsideParent" ) )
741+ {
742+ bool sub = ClayUI . BeginMenu ( "Outside" ) ;
743+ if ( sub )
744+ {
745+ ClayUI . MenuItem ( "Sub Item" ) ;
746+ ClayUI . EndMenu ( ) ;
747+ }
748+ ClayUI . EndPopup ( ) ;
749+ }
750+ } ) ;
751+
752+ // Frame 3: click far outside — should close everything
753+ _fixture . RunFrame ( ( ) =>
754+ {
755+ if ( ClayUI . BeginPopup ( "outsideParent" ) )
756+ {
757+ bool sub = ClayUI . BeginMenu ( "Outside" ) ;
758+ if ( sub )
759+ {
760+ ClayUI . MenuItem ( "Sub Item" ) ;
761+ ClayUI . EndMenu ( ) ;
762+ }
763+ ClayUI . EndPopup ( ) ;
764+ }
765+ } , mousePos : new Vector2 ( 700 , 700 ) , mouseDown : true ) ;
766+
767+ Assert . False ( ClayUI . IsPopupOpen ( "outsideParent" ) , "Parent popup should close on click outside" ) ;
768+ Assert . False ( ClayUI . IsPopupOpen ( "SubMenu_Outside" ) , "Submenu should close on click outside" ) ;
769+ }
770+
771+ [ Fact ]
772+ public void BeginMenu_DisabledDoesNotOpen ( )
773+ {
774+ // Frame 1: open parent, try disabled submenu
775+ _fixture . RunFrame ( ( ) =>
776+ {
777+ ClayUI . OpenPopupAt ( "disabledParent" , new Vector2 ( 50 , 50 ) ) ;
778+ if ( ClayUI . BeginPopup ( "disabledParent" ) )
779+ {
780+ bool sub = ClayUI . BeginMenu ( "DisabledSub" , enabled : false ) ;
781+ if ( sub ) ClayUI . EndMenu ( ) ;
782+ ClayUI . EndPopup ( ) ;
783+ }
784+ } ) ;
785+
786+ Assert . False ( ClayUI . IsPopupOpen ( "SubMenu_DisabledSub" ) ) ;
787+ }
788+
789+ [ Fact ]
790+ public void NestedSubmenus_ThreeLevelsDeep ( )
791+ {
792+ // Open all three levels manually
793+ _fixture . RunFrame ( ( ) =>
794+ {
795+ ClayUI . OpenPopupAt ( "level0" , new Vector2 ( 50 , 50 ) ) ;
796+ if ( ClayUI . BeginPopup ( "level0" ) )
797+ {
798+ ClayUI . OpenPopupAt ( "SubMenu_Level1" , new Vector2 ( 200 , 50 ) ) ;
799+ bool l1 = ClayUI . BeginMenu ( "Level1" ) ;
800+ if ( l1 )
801+ {
802+ ClayUI . OpenPopupAt ( "SubMenu_Level2" , new Vector2 ( 350 , 50 ) ) ;
803+ bool l2 = ClayUI . BeginMenu ( "Level2" ) ;
804+ if ( l2 )
805+ {
806+ ClayUI . MenuItem ( "Deep Item" ) ;
807+ ClayUI . EndMenu ( ) ;
808+ }
809+ ClayUI . EndMenu ( ) ;
810+ }
811+ ClayUI . EndPopup ( ) ;
812+ }
813+ } ) ;
814+
815+ Assert . True ( ClayUI . IsPopupOpen ( "level0" ) ) ;
816+ Assert . True ( ClayUI . IsPopupOpen ( "SubMenu_Level1" ) ) ;
817+ Assert . True ( ClayUI . IsPopupOpen ( "SubMenu_Level2" ) ) ;
818+ }
587819}
0 commit comments