@@ -600,6 +600,166 @@ function renderCircle(node: any, vec2: c_ptr): void {
600600 }
601601}
602602
603+ /**
604+ * Renders a radial menu component - a circular menu with selectable sectors.
605+ * This demonstrates creating custom interactive widgets using ImGui's draw list API.
606+ */
607+ function renderRadialMenu ( node : any , vec2 : c_ptr ) : void {
608+ const props = node . props ;
609+ const drawList = _igGetWindowDrawList ( ) ;
610+
611+ // Get menu properties
612+ const menuRadius = validateNumber ( ( props && props . radius !== undefined ) ? props . radius : 80 , 80 , "radialmenu radius" ) ;
613+ const innerRadius = menuRadius * 0.3 ; // Inner circle is 30% of outer radius
614+
615+ // Get items array - return early if not provided
616+ if ( ! props || ! props . items || ! Array . isArray ( props . items ) ) {
617+ return ;
618+ }
619+ const items : any = props . items ;
620+ const itemCount = items . length ;
621+ if ( itemCount === 0 ) return ;
622+
623+ // Get window cursor position - this is where we'll draw
624+ _igGetCursorScreenPos ( vec2 ) ;
625+ const winX = + get_ImVec2_x ( vec2 ) ;
626+ const winY = + get_ImVec2_y ( vec2 ) ;
627+
628+ // Menu center is offset from top-left by radius (so full circle is visible)
629+ const centerX = winX + menuRadius ;
630+ const centerY = winY + menuRadius ;
631+
632+ // Get mouse position for hover detection
633+ _igGetMousePos ( vec2 ) ;
634+ const mouseX = + get_ImVec2_x ( vec2 ) ;
635+ const mouseY = + get_ImVec2_y ( vec2 ) ;
636+
637+ // Calculate mouse position relative to menu center
638+ const dx = mouseX - centerX ;
639+ const dy = mouseY - centerY ;
640+ const mouseDist = Math . sqrt ( dx * dx + dy * dy ) ;
641+ const mouseAngle = Math . atan2 ( dy , dx ) ;
642+
643+ // Calculate which sector the mouse is hovering over (-1 if none)
644+ let hoveredSector = - 1 ;
645+ if ( mouseDist >= innerRadius && mouseDist <= menuRadius ) {
646+ // Adjust mouse angle to match sector drawing (which starts at -PI/2, top of circle)
647+ // atan2 returns 0 at right (east), we need 0 at top (north)
648+ let adjustedAngle = mouseAngle + 1.57079632679 ; // Add PI/2 to shift origin to top
649+
650+ // Normalize angle to 0-2π range
651+ if ( adjustedAngle < 0 ) adjustedAngle += 6.28318530718 ; // 2*PI
652+
653+ // Calculate sector index (0 starts at top, goes clockwise)
654+ const anglePerSector = 6.28318530718 / itemCount ; // 2*PI / itemCount
655+ hoveredSector = Math . floor ( adjustedAngle / anglePerSector ) ;
656+ }
657+
658+ // Check for clicks
659+ const wasClicked = _igIsMouseClicked_Bool ( _ImGuiMouseButton_Left , 0 ) ;
660+
661+ // Color palette
662+ const baseColor = 0xFF444444 ; // Dark gray
663+ const hoverColor = 0xFF666666 ; // Lighter gray
664+ const borderColor = 0xFF888888 ; // Light gray border
665+ const textColor = 0xFFFFFFFF ; // White text
666+
667+ // Allocate center point buffer
668+ const centerPtr = allocTmp ( _sizeof_ImVec2 ) ;
669+ set_ImVec2_x ( centerPtr , centerX ) ;
670+ set_ImVec2_y ( centerPtr , centerY ) ;
671+
672+ const anglePerSector = 6.28318530718 / itemCount ; // 2*PI / itemCount
673+
674+ // First pass: Draw all filled sectors and outlines
675+ for ( let i = 0 ; i < itemCount ; i ++ ) {
676+ const angleStart = i * anglePerSector - 1.57079632679 ; // Start at top (-PI/2)
677+ const angleEnd = ( i + 1 ) * anglePerSector - 1.57079632679 ;
678+
679+ // Choose color based on hover state
680+ const sectorColor = ( i === hoveredSector ) ? hoverColor : baseColor ;
681+
682+ // Draw filled sector using path API
683+ _ImDrawList_PathClear ( drawList ) ;
684+ _ImDrawList_PathLineTo ( drawList , centerPtr ) ; // Start at center
685+ _ImDrawList_PathArcTo ( drawList , centerPtr , menuRadius , angleStart , angleEnd , 32 ) ;
686+ _ImDrawList_PathLineTo ( drawList , centerPtr ) ; // Back to center
687+ _ImDrawList_PathFillConvex ( drawList , sectorColor ) ;
688+
689+ // Draw sector outline
690+ _ImDrawList_PathClear ( drawList ) ;
691+ _ImDrawList_PathArcTo ( drawList , centerPtr , menuRadius , angleStart , angleEnd , 32 ) ;
692+ _ImDrawList_PathStroke ( drawList , borderColor , 0 , 1.0 ) ;
693+ }
694+
695+ // Second pass: Draw radial lines on top
696+ for ( let i = 0 ; i < itemCount ; i ++ ) {
697+ const angleEnd = ( i + 1 ) * anglePerSector - 1.57079632679 ;
698+ const lineAngle = angleEnd ;
699+ const lineStartX = centerX + Math . cos ( lineAngle ) * innerRadius ;
700+ const lineStartY = centerY + Math . sin ( lineAngle ) * innerRadius ;
701+ const lineEndX = centerX + Math . cos ( lineAngle ) * menuRadius ;
702+ const lineEndY = centerY + Math . sin ( lineAngle ) * menuRadius ;
703+
704+ set_ImVec2_x ( vec2 , lineStartX ) ;
705+ set_ImVec2_y ( vec2 , lineStartY ) ;
706+ const lineEnd = allocTmp ( _sizeof_ImVec2 ) ;
707+ set_ImVec2_x ( lineEnd , lineEndX ) ;
708+ set_ImVec2_y ( lineEnd , lineEndY ) ;
709+ _ImDrawList_AddLine ( drawList , vec2 , lineEnd , borderColor , 1.0 ) ;
710+ }
711+
712+ // Third pass: Draw text labels and handle clicks
713+ for ( let i = 0 ; i < itemCount ; i ++ ) {
714+ const angleStart = i * anglePerSector - 1.57079632679 ;
715+ const labelAngle = angleStart + anglePerSector / 2.0 ;
716+ const labelRadius = innerRadius + ( menuRadius - innerRadius ) * 0.6 ;
717+ const labelX = centerX + Math . cos ( labelAngle ) * labelRadius ;
718+ const labelY = centerY + Math . sin ( labelAngle ) * labelRadius ;
719+
720+ // Calculate text size for centering
721+ const labelText = String ( items [ i ] ) ;
722+ const textSizePtr = allocTmp ( _sizeof_ImVec2 ) ;
723+ _igCalcTextSize ( textSizePtr , tmpUtf8 ( labelText ) , c_null , 0 , - 1.0 ) ;
724+ const textWidth = + get_ImVec2_x ( textSizePtr ) ;
725+ const textHeight = + get_ImVec2_y ( textSizePtr ) ;
726+
727+ // Draw centered text
728+ set_ImVec2_x ( vec2 , labelX - textWidth / 2.0 ) ;
729+ set_ImVec2_y ( vec2 , labelY - textHeight / 2.0 ) ;
730+ _ImDrawList_AddText_Vec2 ( drawList , vec2 , textColor , tmpUtf8 ( labelText ) , c_null ) ;
731+
732+ // Handle click on this sector
733+ if ( wasClicked && i === hoveredSector ) {
734+ safeInvokeCallback ( node . props . onItemClick , i ) ;
735+ }
736+ }
737+
738+ // Draw inner circle (center)
739+ const centerCircleColor = 0xFF333333 ;
740+ _ImDrawList_AddCircleFilled ( drawList , centerPtr , innerRadius , centerCircleColor , 32 ) ;
741+ _ImDrawList_AddCircle ( drawList , centerPtr , innerRadius , borderColor , 32 , 1.0 ) ;
742+
743+ // Draw center text if provided
744+ const centerText = ( props && props . centerText ) ? String ( props . centerText ) : "" ;
745+ if ( centerText !== "" ) {
746+ const centerTextSizePtr = allocTmp ( _sizeof_ImVec2 ) ;
747+ _igCalcTextSize ( centerTextSizePtr , tmpUtf8 ( centerText ) , c_null , 0 , - 1.0 ) ;
748+ const centerTextWidth = + get_ImVec2_x ( centerTextSizePtr ) ;
749+ const centerTextHeight = + get_ImVec2_y ( centerTextSizePtr ) ;
750+
751+ set_ImVec2_x ( vec2 , centerX - centerTextWidth / 2.0 ) ;
752+ set_ImVec2_y ( vec2 , centerY - centerTextHeight / 2.0 ) ;
753+ _ImDrawList_AddText_Vec2 ( drawList , vec2 , textColor , tmpUtf8 ( centerText ) , c_null ) ;
754+ }
755+
756+ // Advance cursor to reserve space
757+ const menuDiameter = menuRadius * 2 ;
758+ set_ImVec2_x ( vec2 , menuDiameter ) ;
759+ set_ImVec2_y ( vec2 , menuDiameter ) ;
760+ _igDummy ( vec2 ) ;
761+ }
762+
603763// Tree traversal and rendering
604764function renderNode ( node : any ) : void {
605765 if ( ! node ) return ;
@@ -691,6 +851,10 @@ function renderNode(node: any): void {
691851 renderCircle ( node , vec2 ) ;
692852 break ;
693853
854+ case "radialmenu" :
855+ renderRadialMenu ( node , vec2 ) ;
856+ break ;
857+
694858 default :
695859 // Unknown type - just render children
696860 if ( node . children ) {
0 commit comments