Editor/UI/RiggerWindow.cs
using Editor;
using Sandbox;
using System.Threading.Tasks;
namespace HumanoidRigger.Editor;

public sealed class RiggerWindow : Widget
{
    public const string DockTitle="Humanoid Rigger";
    public static RiggerWindow Instance {get;private set;}
    public Wizard Session {get;}=new();
    RiggerViewport viewport;Widget content,toolbar,footer,toolbarContent,footerContent;Label status;bool busy;
    ImportedCharacter framedCharacter;WizardStep framedStep;
    JointPanel jointPanel;
    CharacterPose previewPose=CharacterPose.Auto;
    ComboBox poseSelection;
    bool customPose;
    bool updatingPoseSelection;
    IReadOnlyDictionary<string,System.Numerics.Quaternion> editedPose;
    AutomaticHandRefiner handRefiner;
    IReadOnlyDictionary<int,string> previewMaterials=new Dictionary<int,string>();
    internal double LastAdvanceWorkMilliseconds {get;private set;}
    internal double LastAdvanceUiMilliseconds {get;private set;}
    internal bool LastAdvanceWorkerWasMainThread {get;private set;}
    Task<Wizard> preparation;
    Wizard preparingDraft;
    long queuedPreparationRevision=-1;
    FingerCountDialog fingerCountDialog;
    public RiggerWindow(Widget parent):this(parent,true){}
    internal RiggerWindow(Widget parent,bool register):base(parent)
    {
        if(register)Instance=this;WindowTitle=DockTitle;Name="HumanoidRigger";Cursor=CursorShape.Arrow;SetWindowIcon("accessibility_new");MinimumSize=new(1060,780);Size=new(1280,940);Layout=Layout.Column();EnsureHandRefiner();Build();
    }
    [Event("tools.editorwindow.createview")]
    static void RegisterViewMenu(Menu menu)
    {
        // Join the editor's alphabetically sorted tools without creating a dock.
        EditorWindow.DockManager.RegisterDockType(new DockManager.DockInfo
        {
            Title=DockTitle,Icon="accessibility_new",CreateAction=OpenFloatingView
        });
    }
    static Widget OpenFloatingView(){Open();return null;}

    [Event("tools.editorwindow.postcreateview")]
    static void ConfigureViewMenu(Menu menu)
    {
        var option=menu.GetOption(DockTitle);
        if(option is null)return;
        option.Toggled=null;option.Checkable=false;option.Triggered=Open;
    }

    public static void Open()
    {
        if(Instance.IsValid()){ShowExistingWindow(Instance);return;}
        CreateFloatingWindow(DockTitle,true);
    }
    internal static void ShowExistingWindow(RiggerWindow window)
    {
        // Move sessions opened by older versions into the same themed window as new
        // sessions. Preserve the widget, viewport and edits when removing the old dock.
        var dock=EditorWindow.DockManager.FindDockWidget(window);
        if(dock.IsValid())
        {
            var previous=window.GetWindow();var size=previous.Size;var position=previous.Position;bool floating=dock.IsFloating;
            var dialog=CreateFloatingHost(window.WindowTitle);
            // Replacing the dock content releases its native ownership before reparenting.
            dock.Widget=new Widget(dock);
            window.Parent=dialog;dialog.Layout.Add(window,1);
            EditorWindow.DockManager.RemoveDock(dock);dock.Destroy();
            if(floating){dialog.Window.Size=size;dialog.Window.Position=position;}
            dialog.Show();window.Show();dialog.Window.Raise();
            return;
        }
        var existing=window.GetWindow();existing.Show();window.Show();existing.Raise();
    }
    internal static Dialog CreateFloatingWindow(string title,bool register)
    {
        var dialog=CreateFloatingHost(title);
        dialog.Layout.Add(new RiggerWindow(dialog,register),1);dialog.Show();dialog.Window.Size=new(1280,940);return dialog;
    }
    void Build()
    {
        if(Session.Rig is null){previewPose=CharacterPose.Auto;customPose=false;editedPose=null;}
        if(Session.Step==WizardStep.Import)
        {
            content?.Destroy();viewport=null;framedCharacter=null;
            content=Layout.Add(new Widget(this),1);content.Layout=Layout.Column();
            var drop=new ModelDropArea(content,ImportPath);content.Layout.Add(drop,1);status=content.Layout.Add(new Label(content){Visible=false,WordWrap=true});return;
        }
        if(!viewport.IsValid()||!toolbar.IsValid()||!footer.IsValid()||!jointPanel.IsValid())
        {
            if(viewport.IsValid())viewport.Parent=this;
            content?.Destroy();content=Layout.Add(new Widget(this),1);content.Layout=Layout.Column();
            toolbar=content.Layout.Add(new Widget(content));toolbar.Layout=Layout.Column();
            var middle=content.Layout.Add(new Widget(content),1);middle.Layout=Layout.Column();
            viewport=middle.Layout.Add(viewport.IsValid()?viewport:new RiggerViewport(middle,Session),1);
            // Overlay the panel so entering Body does not resize the native
            // render surface. The viewport frames the character beside it.
            jointPanel=new JointPanel(viewport,Session,viewport);viewport.Changed=LandmarksChanged;
            // Reserve the final review's space throughout the wizard. Changing
            // warnings or progress text must not repeatedly resize the swap chain.
            footer=content.Layout.Add(new Widget(content){FixedHeight=160});footer.Layout=Layout.Column();
        }
        viewport.PoseEdited=PoseEdited;
        viewport.Resized=PositionJointPanel;
        toolbarContent?.Destroy();footerContent?.Destroy();
        toolbarContent=toolbar.Layout.Add(new Widget(toolbar));toolbarContent.Layout=Layout.Row();
        footerContent=footer.Layout.Add(new Widget(footer));footerContent.Layout=Layout.Column();
        var top=toolbarContent.Layout;top.Margin=8;top.Spacing=8;
        top.Add(new Label(content){Text="Profile:"});
        var profile=top.Add(new ComboBox(content){MinimumWidth=190,ToolTip="The target skeleton’s bone names and hierarchy. Hand detection is independent of this choice."});
        foreach(var p in Profiles.BuiltIn.Concat(ProfileStore.Load()))profile.AddItem(p.Name,"person",()=>{Session.SetProfile(p);Build();},selected:p.Id==Session.Profile.Id);
        profile.AddItem("Create Custom Profile…","add",CreateProfile);
        profile.AddItem("Load Profile…","folder_open",LoadProfile);
        if(Session.Rig is not null)
        {
            top.Add(new Label(content){Text="Preview:"});
            poseSelection=top.Add(new ComboBox(content){MinimumWidth=120,ToolTip="Preview the generated rig in a standard pose."});
            foreach(var (label,value) in new[]{("Original Pose",CharacterPose.Auto),("T-Pose",CharacterPose.TPose),("A-Pose 1",CharacterPose.APose1),("A-Pose 2",CharacterPose.APose2)})
                poseSelection.AddItem(label,"accessibility",()=>SetPreviewPose(value),selected:!customPose&&value==previewPose);
            if(editedPose is not null)poseSelection.AddItem("Custom Pose","touch_app",ShowEditedPose,selected:customPose);
        }
        top.AddStretchCell();top.Add(new Button("Replace model","folder_open"){Clicked=SelectModel});top.Add(new Button("Restart","restart_alt"){Clicked=RestartWorkflow});
        viewport.MaterialPaths=previewMaterials;ShowPreview();
        jointPanel.Visible=Session.Step!=WizardStep.Centerline;
        PositionJointPanel();
        jointPanel.Reload();
        if(framedCharacter!=Session.Character||framedStep!=Session.Step){viewport.Frame();framedCharacter=Session.Character;framedStep=Session.Step;}
        var bottom=footerContent.Layout;bottom.Margin=12;bottom.Spacing=8;
        status=bottom.Add(new Label(content){WordWrap=true,Text=Instruction()});
        if(Session.Step is WizardStep.Centerline or WizardStep.Body)
        {
            var importWarnings=Session.Character.ImportWarnings.Where(w=>!w.StartsWith("Detected a Z-up")).ToArray();
            if(importWarnings.Length>0)
            {
                var warning=bottom.Add(new Label(content){Name="ImportMaterialWarning",Text=string.Join("\n",importWarnings),WordWrap=true});
                warning.SetStyles($"color: {Theme.Yellow.Hex};");
            }
        }
        if(Session.Step is WizardStep.Centerline or WizardStep.Body && Session.Anatomy!.UnrecommendedImportPose)
        {
            var warning=bottom.Add(new Label(content){Name="ImportPoseWarning",Text=ImportPose.Warning,WordWrap=true});
            warning.SetStyles($"color: {Theme.Yellow.Hex};");
        }
        if(Session.Step is WizardStep.LeftHand or WizardStep.RightHand)
        {
            var side=Session.Step==WizardStep.LeftHand?"L":"R";
            foreach(var warning in Session.Anatomy!.Warnings.Where(w=>w.StartsWith(side+" hand")||w.StartsWith(side+" fingers")))
            {var label=bottom.Add(new Label(content){Text=warning,WordWrap=true});label.SetStyles($"color: {Theme.Yellow.Hex};");}
        }
        if(Session.Step==WizardStep.Finish)
        {
            bottom.Add(new Label(content){Text=$"Profile: {Session.Profile.Name}"});
            var deformationWarnings=Session.Rig!.Report.Issues.Where(i=>i.Code=="surface-reversal").Select(i=>i.Message).ToArray();
            if(deformationWarnings.Length>0)
            {
                var warning=bottom.Add(new Label(content){Name="DeformationWarning",Text="Some test poses need review. Open Advanced Edit for details.",WordWrap=true});
                warning.SetStyles($"color: {Theme.Yellow.Hex};");
            }
            var checks=bottom.AddRow();checks.Spacing=8;
            foreach(var label in new[]{"Body","Left Hand","Right Hand","Skeleton","Skinning","Validation"})
            {
                string side=label=="Left Hand"?"L":label=="Right Hand"?"R":null;
                var warnings=label=="Validation"?deformationWarnings:side is null?[]:Session.Anatomy!.Warnings.Where(w=>w.StartsWith(side+" hand")||w.StartsWith(side+" fingers")).ToArray();
                checks.Add(new StatusChip(content,label,warnings.Length>0?Theme.Yellow:Theme.Green){ToolTip=warnings.Length>0?string.Join("\n",warnings):"Checked"});
            }
            checks.AddStretchCell();
            var row=bottom.AddRow();row.Spacing=8;
            row.Add(new Button("Back","arrow_back"){Clicked=GoBack});
            row.Add(new Button("Reset Pose","restart_alt"){Clicked=()=>SetPreviewPose(CharacterPose.Auto)});
            row.Add(new Button("Test Rig","play_arrow"){Clicked=TestRig});row.Add(new Button("Advanced Edit","tune"){Clicked=Advanced});row.AddStretchCell();row.Add(new Button.Primary("Save"){Icon="check",Tint=Theme.Green,Clicked=Save});
        }
        else
        {
            var row=bottom.AddRow();row.Spacing=8;
            if(Session.Step!=WizardStep.Centerline)row.Add(new Button("Back","arrow_back"){Clicked=GoBack});
            row.Add(new Button("Reset","restart_alt"){Clicked=()=>{Session.Reset();Build();}});row.AddStretchCell();
            row.Add(new Button.Primary("Continue"){Enabled=Session.Step!=WizardStep.Validation,Clicked=Continue});
        }
        SchedulePreparation();
    }
    void PositionJointPanel()
    {
        if(!viewport.IsValid()||!jointPanel.IsValid())return;
        jointPanel.Position=new(viewport.Width-jointPanel.FixedWidth,0);
        jointPanel.Size=new(jointPanel.FixedWidth,viewport.Height);
        viewport.RightInset=jointPanel.Visible?jointPanel.FixedWidth:0;
        jointPanel.Raise();
    }
    void GoBack(){Session.Back();Build();}
    string Instruction()=>Session.Step switch
    {
        WizardStep.Centerline=>"Check the centerline.\nDrag the line left or right to adjust it.",
        WizardStep.Body=>"Check the points.\nMove any point that is incorrect.",WizardStep.LeftHand=>"Check the left hand.\nMove any incorrect points.",WizardStep.RightHand=>"Check the right hand.\nMove any incorrect points.",WizardStep.Finish=>"Rig Complete\nDrag a bone to test the rig.",
        WizardStep.Validation=>string.Join("\n",Session.Rig!.Report.Issues.Where(i=>i.Error).Select(i=>i.Message)),_=>"Generating rig…"
    };
    void SelectModel(){var path=EditorUtility.OpenFileDialog("Select model",ModelImporter.FileFilter,"");if(!string.IsNullOrEmpty(path))ImportPath(path);}
    void CreateProfile()
    {
        var path=EditorUtility.OpenFileDialog("Select rigged character","Rigged characters (*.fbx *.gltf *.glb)","");if(string.IsNullOrEmpty(path))return;
        try{new ProfileDialog(this,ModelImporter.Import(path),p=>{Session.SetProfile(p);Build();}).Show();}catch(Exception e){Error(e);}
    }
    void LoadProfile()
    {
        var path=EditorUtility.OpenFileDialog("Select rig profile","json","");if(string.IsNullOrEmpty(path))return;
        try{var p=RigProfile.FromJson(File.ReadAllText(path));ProfileStore.Save(p);Session.SetProfile(p);Build();}catch(Exception e){Error(e);}
    }
    public async void ImportPath(string path)
    {
        try{await ImportAsync(path);}catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}
    }
    public async Task ImportAsync(string path)
    {
        if(busy)throw new InvalidOperationException("The current operation is still running.");
        EnsureHandRefiner();
        SetBusy(true);
        try
        {
            await RigWork.Run(()=>Session.Import(path));await new EditorThread();
            if(!this.IsValid())return;
            previewMaterials=new Dictionary<int,string>();
            string textureWarning=null;
            try{previewMaterials=await MaterialAssets.Preview(Session.Character);}
            catch(Exception e){textureWarning="Textures: "+e.Message;Log.Warning(textureWarning);}
            await new EditorThread();if(this.IsValid()){Build();if(textureWarning is not null){status.Text+="\n"+textureWarning;status.SetStyles($"color: {Theme.Yellow.Hex};");}}
        }
        finally{await new EditorThread();SetBusy(false);}
    }
    void Continue()
    {
        if(busy)return;
        if(Session.Step==WizardStep.Body)
        {
            if(fingerCountDialog.IsValid()&&fingerCountDialog.Visible){fingerCountDialog.Window.Raise();return;}
            var revision=Session.Revision;
            fingerCountDialog=new FingerCountDialog(this,Session.LeftFingerCount,Session.RightFingerCount,(left,right)=>
            {
                if(!this.IsValid()||Session.Step!=WizardStep.Body||Session.Revision!=revision)return;
                Session.SetFingerCounts(left,right);AdvanceFromControls();
            });
            fingerCountDialog.Show();return;
        }
        AdvanceFromControls();
    }
    async void AdvanceFromControls()
    {
        try{await AdvanceAsync();}catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}
    }
    public async Task AdvanceAsync()
    {
        if(busy)throw new InvalidOperationException("The current operation is still running.");
        EnsureHandRefiner();
        SetBusy(true);
        status.Text=Session.Step switch{WizardStep.Body=>"Preparing left hand…",WizardStep.LeftHand=>"Preparing right hand…",WizardStep.RightHand=>"Generating rig…",_=>Instruction()};
        try
        {
            var started=System.Diagnostics.Stopwatch.GetTimestamp();
            if(preparation is not null&&!preparation.IsCompleted&&!Session.CanAccept(preparingDraft))
            {try{await preparation.ConfigureAwait(false);}catch{}await new EditorThread();}
            Wizard result;
            if(Session.Step==WizardStep.RightHand)
            {
                // Give the busy state a frame to appear, then generate on the
                // editor thread. Keep a draft so a failure preserves the edits.
                await Task.Delay(16).ConfigureAwait(false);await new EditorThread();
                if(!this.IsValid())return;
                result=Session.CopyForContinuation();
                LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;
                result.Continue();
            }
            else
            {
                if(preparation is null||!Session.CanAccept(preparingDraft)||preparation.IsFaulted)StartPreparation();
                result=await preparation.ConfigureAwait(false);await new EditorThread();
            }
            Session.AcceptContinuation(result);
            LastAdvanceWorkMilliseconds=System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;
            started=System.Diagnostics.Stopwatch.GetTimestamp();if(this.IsValid())Build();
            LastAdvanceUiMilliseconds=System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;
        }
        finally{await new EditorThread();SetBusy(false);SchedulePreparation();}
    }
    public void CaptureViewport(string path)=>viewport.Capture(path);
    internal void RefreshDisplay()
    {
        // Rebind native event handlers after editor hot reload without replacing the session.
        viewport?.Destroy();viewport=null;framedCharacter=null;MinimumSize=new(1060,780);Build();
    }
    void ShowPreview()
    {
        if(Session.Rig is null){viewport.ShowCharacter();return;}
        viewport.SetPose(customPose?editedPose:RigPosePreview.Rotations(Session.Rig,previewPose),false);
    }
    public void SetPreviewPose(CharacterPose pose)
    {
        if(updatingPoseSelection)return;
        if(busy)throw new InvalidOperationException("The current operation is still running.");
        if(Session.Rig is null)throw new InvalidOperationException("Generate the rig before previewing poses.");
        if(pose is not (CharacterPose.Auto or CharacterPose.TPose or CharacterPose.APose1 or CharacterPose.APose2))throw new ArgumentException("Unsupported preview pose.");
        previewPose=pose;customPose=false;viewport.SetPose(RigPosePreview.Rotations(Session.Rig,pose));
        var label=pose switch{CharacterPose.Auto=>"Original Pose",CharacterPose.TPose=>"T-Pose",CharacterPose.APose1=>"A-Pose 1",_=>"A-Pose 2"};
        SelectPoseLabel(label);
        status.Text=Instruction();
    }
    void PoseEdited(IReadOnlyDictionary<string,System.Numerics.Quaternion> pose)
    {
        customPose=true;editedPose=new Dictionary<string,System.Numerics.Quaternion>(pose);
        SelectPoseLabel("Custom Pose");
    }
    void SelectPoseLabel(string label)
    {
        if(!poseSelection.IsValid())return;
        // Native ComboBox selection invokes its action even for programmatic
        // changes. Updating the label must not restart or cancel an active drag.
        updatingPoseSelection=true;
        try
        {
            if(poseSelection.FindIndex(label) is {} index)poseSelection.CurrentIndex=index;
            else if(label=="Custom Pose")poseSelection.AddItem(label,"touch_app",ShowEditedPose,selected:true);
        }
        finally{updatingPoseSelection=false;}
    }
    void ShowEditedPose(){if(updatingPoseSelection||editedPose is null||Session.Rig is null)return;customPose=true;viewport.SetPose(editedPose);}
    public void RestartWorkflow()
    {
        if(busy)throw new InvalidOperationException("The current operation is still running.");
        Session.Restart();Build();
    }
    void Error(Exception e){status.Visible=true;status.Text=e.Message;status.SetStyles($"color: {Theme.Red.Hex};");}
    void SetBusy(bool value)
    {
        busy=value;if(!this.IsValid())return;
        if(viewport.IsValid())
        {
            viewport.AllowEditing=!value;
            if(toolbar.IsValid())toolbar.Enabled=!value;if(footer.IsValid())footer.Enabled=!value;if(jointPanel.IsValid())jointPanel.Enabled=!value;
        }
        else if(content.IsValid())content.Enabled=!value;
    }
    async void TestRig()
    {
        if(busy)return;SetBusy(true);
        try
        {
            foreach(var pose in Deformation.Poses)
            {
                await new EditorThread();if(!this.IsValid()||Session.Rig is null)break;
                if(!Deformation.IsApplicable(pose,Session.Rig.Bones.Select(b=>b.Role).ToHashSet()))continue;
                viewport.SetPose(Deformation.JointRotations(Session.Rig,pose));status.Text=pose.Name;await Task.Delay(650);
            }
            await new EditorThread();if(this.IsValid()){viewport.SetPose(customPose?editedPose:RigPosePreview.Rotations(Session.Rig,previewPose));status.Text=Instruction();}
        }
        catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}finally{await new EditorThread();SetBusy(false);}
    }
    void Advanced()
    {
        var dialog=new Dialog(this);dialog.Window.WindowTitle="Rig details";dialog.Window.MinimumSize=new(560,480);dialog.Layout=Layout.Column();dialog.Layout.Margin=12;dialog.Layout.Spacing=8;
        var scroll=new ScrollArea(dialog);scroll.Canvas=new Widget(scroll);scroll.Canvas.Layout=Layout.Column();
        foreach(var b in Session.Rig!.Bones)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=$"{b.Name}  ·  {b.Role}  ·  {b.Position}"});
        foreach(var issue in Session.Rig.Report.Issues)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=issue.Message});
        foreach(var hand in Session.Anatomy!.HandRefinements)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=$"{(hand.Key=="L"?"Left":"Right")} hand: {hand.Value.Status}"});
        dialog.Layout.Add(scroll,1);dialog.Show();
    }
    void Save()
    {
        if(busy)return;
        new SaveRigDialog(this,Session,result=>{if(this.IsValid()){status.Visible=true;status.Text="Saved "+string.Join(" + ",result.Files.Select(Path.GetFileName));status.ToolTip=string.Join("\n",result.Files);status.SetStyles($"color: {Theme.Green.Hex};");}}).Show();
    }
    void EnsureHandRefiner(){if(handRefiner is null){handRefiner=new AutomaticHandRefiner();handRefiner.Warmup();}Session.HandRefiner=handRefiner;}
    public override void OnDestroyed(){handRefiner?.Dispose();if(Instance==this)Instance=null;base.OnDestroyed();}
    static Dialog CreateFloatingHost(string title)
    {
        var dialog=new Dialog(null);dialog.Window.Title=title;dialog.Window.SetWindowIcon("accessibility_new");
        dialog.Layout=Layout.Column();dialog.Window.Size=new(1280,940);return dialog;
    }
    void LandmarksChanged(){jointPanel.RefreshRows();SchedulePreparation();}
    void StartPreparation()
    {
        preparingDraft=Session.CopyForContinuation();var draft=preparingDraft;
        preparation=RigWork.Run(()=>{LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;draft.Continue();return draft;});
        _=preparation.ContinueWith(task=>{_ = task.Exception;},TaskContinuationOptions.OnlyOnFaulted);
    }
    void SchedulePreparation()
    {
        // Preparing hands is cheap; generating a full rig while its final hand
        // is still being edited wastes both memory and a complete repair pass.
        if(!this.IsValid()||Session.Step is not (WizardStep.Body or WizardStep.LeftHand)||queuedPreparationRevision==Session.Revision)return;
        queuedPreparationRevision=Session.Revision;_=PrepareWhenIdle(Session.Revision);
    }
    async Task PrepareWhenIdle(long revision)
    {
        // Coalesce marker drags. At most one solver job per window may run at a time.
        await Task.Delay(150).ConfigureAwait(false);await new EditorThread();
        if(!this.IsValid()||busy||Session.Revision!=revision)return;
        if(preparation is not null&&!preparation.IsCompleted)
        {try{await preparation.ConfigureAwait(false);}catch{}await new EditorThread();}
        if(!this.IsValid()||busy||Session.Revision!=revision)return;
        if(preparingDraft is null||!Session.CanAccept(preparingDraft))StartPreparation();
    }
}

sealed class ModelDropArea:Widget
{
    readonly Action<string> import;int hover;
    public ModelDropArea(Widget parent,Action<string> import):base(parent)
    {
        this.import=import;AcceptDrops=true;Layout=Layout.Column();Layout.Margin=12;Layout.Spacing=8;Layout.AddStretchCell();
        var row=Layout.AddRow();row.AddStretchCell();var center=row.AddColumn();center.Spacing=12;
        center.Add(new DropFolderIcon(this));
        center.Add(new Label(this){Text="Please drag and drop a character file here (.fbx, .obj, .gltf, .glb)",Alignment=TextFlag.Center});
        center.Add(new Label(this){Text="or",Alignment=TextFlag.Center});
        var choice=center.AddRow();choice.AddStretchCell();
        choice.Add(new Button.Primary("Choose File"){MinimumWidth=120,Clicked=()=>{var path=EditorUtility.OpenFileDialog("Choose File",ModelImporter.FileFilter,"");if(!string.IsNullOrEmpty(path))import(path);}});
        choice.AddStretchCell();
        row.AddStretchCell();Layout.AddStretchCell();
    }
    public override void OnDragHover(DragEvent e)
    {
        bool valid=e.Data.HasFileOrFolder&&ModelImporter.CanImport(e.Data.FileOrFolder);hover=valid?1:-1;if(valid)e.Action=DropAction.Link;Update();
    }
    public override void OnDragDrop(DragEvent e){hover=0;if(e.Data.HasFileOrFolder&&ModelImporter.CanImport(e.Data.FileOrFolder)){e.Action=DropAction.Link;import(e.Data.FileOrFolder);}Update();}
    public override void OnDragLeave(){hover=0;Update();}
    protected override void OnPaint(){Paint.SetPen(hover==1?Theme.Green:hover<0?Theme.Red:Theme.ControlBackground.Lighten(.2f),1);Paint.SetBrush(hover==1?Theme.Green.WithAlpha(.06f):Paint.HasMouseOver?Theme.ControlBackground.Lighten(.3f):Theme.ControlBackground);Paint.DrawRect(LocalRect.Shrink(12),4);}
}

sealed class DropFolderIcon : Widget
{
    public DropFolderIcon(Widget parent):base(parent){FixedHeight=48;}
    protected override void OnPaint()
    {
        Paint.SetPen(Theme.TextLight);
        Paint.DrawIcon(new Rect((Width-40)*.5f,4,40,40),"create_new_folder",40);
    }
}