@@ -20,6 +20,7 @@ import AudioEditorView from './components/AudioEditorView';
2020import CharacterEditorView from './components/CharacterEditorView' ;
2121import SceneComposer from './components/SceneComposer' ;
2222import ImageMapComposer from './components/ImageMapComposer' ;
23+ import ScreenLayoutComposer from './components/ScreenLayoutComposer' ;
2324import MarkdownPreviewView from './components/MarkdownPreviewView' ;
2425import PunchlistManager from './components/PunchlistManager' ;
2526import TabContextMenu from './components/TabContextMenu' ;
@@ -37,7 +38,7 @@ import type {
3738 Block , BlockGroup , Link , Position , FileSystemTreeNode , EditorTab ,
3839 ToastMessage , IdeSettings , Theme , ProjectImage , RenpyAudio ,
3940 ClipboardState , ImageMetadata , AudioMetadata , LabelNode , Character ,
40- AppSettings , ProjectSettings , StickyNote , SceneComposition , SceneSprite , ImageMapComposition , PunchlistMetadata , MouseGestureSettings ,
41+ AppSettings , ProjectSettings , StickyNote , SceneComposition , SceneSprite , ImageMapComposition , ScreenLayoutComposition , PunchlistMetadata , MouseGestureSettings ,
4142 ProjectLoadResult , ScannedImageAsset , ScannedAudioAsset , SerializedSprite , SerializedSceneComposition , UserSnippet
4243} from './types' ;
4344import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' ;
@@ -379,6 +380,9 @@ const App: React.FC = () => {
379380 // ImageMap Composer State
380381 const [ imagemapCompositions , setImagemapCompositions ] = useImmer < Record < string , ImageMapComposition > > ( { } ) ;
381382
383+ // Screen Layout Composer State
384+ const [ screenLayoutCompositions , setScreenLayoutCompositions ] = useImmer < Record < string , ScreenLayoutComposition > > ( { } ) ;
385+
382386 // Punchlist State
383387 const [ punchlistMetadata , setPunchlistMetadata ] = useImmer < Record < string , PunchlistMetadata > > ( { } ) ;
384388
@@ -675,6 +679,82 @@ const App: React.FC = () => {
675679 setHasUnsavedSettings ( true ) ;
676680 } , [ setImagemapCompositions , activeTabId ] ) ;
677681
682+ // --- Screen Layout Composer Management ---
683+ const handleCreateScreenLayout = useCallback ( ( initialName ?: string ) => {
684+ const id = `screenlayout-${ Date . now ( ) } ` ;
685+ const name = initialName || `screen_${ Object . keys ( screenLayoutCompositions ) . length + 1 } ` ;
686+
687+ setScreenLayoutCompositions ( draft => {
688+ draft [ id ] = {
689+ screenName : name ,
690+ gameWidth : 1920 ,
691+ gameHeight : 1080 ,
692+ modal : false ,
693+ zorder : 0 ,
694+ widgets : [ ]
695+ } ;
696+ } ) ;
697+
698+ setOpenTabs ( prev => [ ...prev , { id, type : 'screen-layout-composer' , layoutId : id } ] ) ;
699+ setActiveTabId ( id ) ;
700+ setHasUnsavedSettings ( true ) ;
701+ } , [ screenLayoutCompositions , setScreenLayoutCompositions ] ) ;
702+
703+ const handleOpenScreenLayout = useCallback ( ( layoutId : string ) => {
704+ setOpenTabs ( prev => {
705+ if ( ! prev . find ( t => t . id === layoutId ) ) {
706+ return [ ...prev , { id : layoutId , type : 'screen-layout-composer' , layoutId } ] ;
707+ }
708+ return prev ;
709+ } ) ;
710+ setActiveTabId ( layoutId ) ;
711+ } , [ ] ) ;
712+
713+ const handleScreenLayoutUpdate = useCallback ( ( layoutId : string , value : React . SetStateAction < ScreenLayoutComposition > ) => {
714+ setScreenLayoutCompositions ( draft => {
715+ const prev = draft [ layoutId ] || { screenName : '' , gameWidth : 1920 , gameHeight : 1080 , modal : false , zorder : 0 , widgets : [ ] } ;
716+ const next = typeof value === 'function' ? ( value as ( prevState : ScreenLayoutComposition ) => ScreenLayoutComposition ) ( prev ) : value ;
717+
718+ if ( JSON . stringify ( prev ) !== JSON . stringify ( next ) ) {
719+ draft [ layoutId ] = next ;
720+ setHasUnsavedSettings ( true ) ;
721+ }
722+ } ) ;
723+ } , [ setScreenLayoutCompositions ] ) ;
724+
725+ const handleRenameScreenLayout = useCallback ( ( layoutId : string , newName : string ) => {
726+ setScreenLayoutCompositions ( draft => {
727+ if ( draft [ layoutId ] && draft [ layoutId ] . screenName !== newName ) {
728+ draft [ layoutId ] . screenName = newName ;
729+ setHasUnsavedSettings ( true ) ;
730+ }
731+ } ) ;
732+ } , [ setScreenLayoutCompositions ] ) ;
733+
734+ const handleDuplicateScreenLayout = useCallback ( ( layoutId : string ) => {
735+ const original = screenLayoutCompositions [ layoutId ] ;
736+ if ( ! original ) return ;
737+ const existingNames = new Set ( Object . values ( screenLayoutCompositions ) . map ( c => c . screenName ) ) ;
738+ let counter = 1 ;
739+ let newName = `${ original . screenName } (${ counter } )` ;
740+ while ( existingNames . has ( newName ) ) { counter ++ ; newName = `${ original . screenName } (${ counter } )` ; }
741+ const newId = `screenlayout-${ Date . now ( ) } ` ;
742+ setScreenLayoutCompositions ( draft => { draft [ newId ] = { ...original , screenName : newName } ; } ) ;
743+ setHasUnsavedSettings ( true ) ;
744+ setOpenTabs ( prev => {
745+ if ( ! prev . find ( t => t . id === newId ) ) return [ ...prev , { id : newId , type : 'screen-layout-composer' , layoutId : newId } ] ;
746+ return prev ;
747+ } ) ;
748+ setActiveTabId ( newId ) ;
749+ } , [ screenLayoutCompositions , setScreenLayoutCompositions ] ) ;
750+
751+ const handleDeleteScreenLayout = useCallback ( ( layoutId : string ) => {
752+ setScreenLayoutCompositions ( draft => { delete draft [ layoutId ] ; } ) ;
753+
754+ setOpenTabs ( prev => prev . filter ( t => t . id !== layoutId ) ) ;
755+ if ( activeTabId === layoutId ) setActiveTabId ( 'canvas' ) ;
756+ setHasUnsavedSettings ( true ) ;
757+ } , [ setScreenLayoutCompositions , activeTabId ] ) ;
678758
679759 // --- Sync Explorer with Active Tab ---
680760 useEffect ( ( ) => {
@@ -1336,6 +1416,13 @@ const App: React.FC = () => {
13361416 setImagemapCompositions ( { } ) ;
13371417 }
13381418
1419+ // Restore Screen Layout Compositions
1420+ if ( projectData . settings . screenLayoutCompositions ) {
1421+ setScreenLayoutCompositions ( projectData . settings . screenLayoutCompositions ) ;
1422+ } else {
1423+ setScreenLayoutCompositions ( { } ) ;
1424+ }
1425+
13391426 // Restore Scan Directories
13401427 if ( projectData . settings . scannedImagePaths ) {
13411428 const paths = projectData . settings . scannedImagePaths ;
@@ -1507,7 +1594,7 @@ const App: React.FC = () => {
15071594 setLoadingMessage ( '' ) ;
15081595 setLoadingProgress ( 0 ) ;
15091596 }
1510- } , [ setBlocks , setImages , setAudios , updateProjectSettings , addToast , setFileSystemTree , setStickyNotes , setCharacterProfiles , updateAppSettings , setSceneCompositions , setSceneNames , setPunchlistMetadata ] ) ;
1597+ } , [ setBlocks , setImages , setAudios , updateProjectSettings , addToast , setFileSystemTree , setStickyNotes , setCharacterProfiles , updateAppSettings , setSceneCompositions , setSceneNames , setPunchlistMetadata , setImagemapCompositions , setScreenLayoutCompositions ] ) ;
15111598
15121599
15131600 const handleCancelLoad = useCallback ( ( ) => {
@@ -1846,6 +1933,7 @@ const App: React.FC = () => {
18461933 sceneCompositions : serializableScenes as unknown as Record < string , SceneComposition > ,
18471934 sceneNames,
18481935 imagemapCompositions : serializableImagemaps ,
1936+ screenLayoutCompositions,
18491937 scannedImagePaths : Array . from ( imageScanDirectories . keys ( ) ) ,
18501938 scannedAudioPaths : Array . from ( audioScanDirectories . keys ( ) ) ,
18511939 } ;
@@ -1856,7 +1944,7 @@ const App: React.FC = () => {
18561944 console . error ( "Failed to save IDE settings:" , e ) ;
18571945 addToast ( 'Failed to save workspace settings' , 'error' ) ;
18581946 }
1859- } , [ projectRootPath , projectSettings , openTabs , activeTabId , splitLayout , splitPrimarySize , secondaryOpenTabs , secondaryActiveTabId , stickyNotes , characterProfiles , addToast , sceneCompositions , sceneNames , imagemapCompositions , imageScanDirectories , audioScanDirectories , punchlistMetadata ] ) ;
1947+ } , [ projectRootPath , projectSettings , openTabs , activeTabId , splitLayout , splitPrimarySize , secondaryOpenTabs , secondaryActiveTabId , stickyNotes , characterProfiles , addToast , sceneCompositions , sceneNames , imagemapCompositions , screenLayoutCompositions , imageScanDirectories , audioScanDirectories , punchlistMetadata ] ) ;
18601948
18611949
18621950 const handleSaveAll = useCallback ( async ( ) => {
@@ -2816,6 +2904,7 @@ const App: React.FC = () => {
28162904 if ( tab . type === 'ai-generator' ) return 'AI Generator' ;
28172905 if ( tab . type === 'scene-composer' ) return sceneNames [ tab . sceneId ! ] || 'Scene' ;
28182906 if ( tab . type === 'imagemap-composer' ) return imagemapCompositions [ tab . imagemapId ! ] ?. screenName || 'ImageMap' ;
2907+ if ( tab . type === 'screen-layout-composer' ) return screenLayoutCompositions [ tab . layoutId ! ] ?. screenName || 'Screen Layout' ;
28192908 if ( tab . type === 'character' ) return `Char: ${ analysisResult . characters . get ( tab . characterTag ! ) ?. name || tab . characterTag } ` ;
28202909 if ( tab . type === 'editor' ) return blocks . find ( b => b . id === tab . blockId ) ?. title || 'Untitled' ;
28212910 if ( tab . type === 'markdown' ) return tab . filePath ?. split ( '/' ) . pop ( ) ?? 'Markdown' ;
@@ -2932,6 +3021,30 @@ const App: React.FC = () => {
29323021 labels = { allLabels }
29333022 /> ;
29343023 }
3024+ if ( tab . type === 'screen-layout-composer' && tab . layoutId ) {
3025+ const composition = screenLayoutCompositions [ tab . layoutId ] || {
3026+ screenName : 'new_screen' ,
3027+ gameWidth : 1920 ,
3028+ gameHeight : 1080 ,
3029+ modal : false ,
3030+ zorder : 0 ,
3031+ widgets : [ ]
3032+ } ;
3033+ const isLayoutLocked = analysisResult . screens . has ( composition . screenName ) ;
3034+ return < ScreenLayoutComposer
3035+ composition = { composition }
3036+ onCompositionChange = { ( val ) => handleScreenLayoutUpdate ( tab . layoutId ! , val ) }
3037+ screenName = { composition . screenName }
3038+ onRenameScreen = { ( newName ) => handleRenameScreenLayout ( tab . layoutId ! , newName ) }
3039+ labels = { Object . keys ( analysisResult . labels ) }
3040+ isLocked = { isLayoutLocked }
3041+ onDuplicate = { ( ) => handleDuplicateScreenLayout ( tab . layoutId ! ) }
3042+ onGoToCode = { isLayoutLocked ? ( ) => {
3043+ const def = analysisResult . screens . get ( composition . screenName ) ;
3044+ if ( def ) handleOpenEditor ( def . definedInBlockId , def . line ) ;
3045+ } : undefined }
3046+ /> ;
3047+ }
29353048 if ( tab . type === 'markdown' && tab . filePath ) {
29363049 return < MarkdownPreviewView
29373050 filePath = { tab . filePath }
@@ -3235,7 +3348,6 @@ const App: React.FC = () => {
32353348 }
32363349 } }
32373350 onFindVariableUsages = { ( name ) => handleFindUsages ( name , 'variable' ) }
3238- onAddScreen = { ( name ) => handleCreateBlockConfirm ( name , 'screen' , 'game' ) }
32393351 onFindScreenDefinition = { ( name ) => {
32403352 const def = analysisResult . screens . get ( name ) ;
32413353 if ( def ) handleOpenEditor ( def . definedInBlockId , def . line ) ;
@@ -3398,6 +3510,12 @@ const App: React.FC = () => {
33983510 onOpenImageMap = { handleOpenImageMap }
33993511 onCreateImageMap = { handleCreateImageMap }
34003512 onDeleteImageMap = { handleDeleteImageMap }
3513+ // Screen Layout Props
3514+ screenLayouts = { Object . keys ( screenLayoutCompositions ) . map ( id => ( { id, name : screenLayoutCompositions [ id ] ?. screenName || 'Screen Layout' } ) ) }
3515+ onOpenScreenLayout = { handleOpenScreenLayout }
3516+ onCreateScreenLayout = { handleCreateScreenLayout }
3517+ onDeleteScreenLayout = { handleDeleteScreenLayout }
3518+ onDuplicateScreenLayout = { handleDuplicateScreenLayout }
34013519 // Snippet Props
34023520 snippetCategoriesState = { snippetCategoriesState }
34033521 onToggleSnippetCategory = { handleToggleSnippetCategory }
0 commit comments