001package polaris.sdk.ui.controls;
002
003import polaris.sdk.ui.*;
004import java.util.ArrayList;
005import java.util.List;
006import java.util.function.DoubleConsumer;
007import java.util.function.DoubleSupplier;
008
009/** Live LiquidGlass controls bound to the attached Ui's theme and standalone background.
010 * No duplicate parameter snapshot: edits from this panel, application code and presets stay in sync.
011 * This panel draws no extra glass backplate. Embed it in an existing Panel/ScrollPane or Dialog.
012 */
013public class GlassSettingsPanel extends Panel {
014    private final List<Parameter> parameters=new ArrayList<Parameter>();
015    private final Switch blurEnabled=new Switch("背景模糊");
016    private final Switch grid=new Switch("网格背景");
017    private final Switch animated=new Switch("背景动画");
018    private final List<Button> presets=new ArrayList<Button>();
019    private final List<Button> templates=new ArrayList<Button>();
020    private final Label templateHeading=new Label("材质模板").size(12);
021    private final Label note=new Label("高光与色散独立调节,默认已开启。\n强模糊也能保留边缘色散;开启网格更容易观察,纯色背景不会凭空产生彩边。").size(11);
022    private float rememberedSigma=18;
023    private static final float ROW=70,BUTTON_HEIGHT=38,TEMPLATE_HEADING_HEIGHT=24;
024    private static final int PARAMETER_COUNT=8,TEMPLATE_COUNT=3,PRESET_COUNT=4;
025
026    public GlassSettingsPanel(){
027        glass(false);padding(0);gap(Spacing.GAP);w=696;h=preferredHeight(w);
028        blurEnabled.id("materialBlurEnabled");grid.id("materialGrid");animated.id("materialAnimate");
029        blurEnabled.tooltip("关闭时 sigma=0;重新开启恢复上次的 sigma。不会覆盖模糊混合比例。");
030        grid.tooltip("真实切换独立示例的 GPU 网格背景;宿主接入自行绘制其背景。");
031        animated.tooltip("暂停在当前背景帧,再开启后继续,不跳回时间零点。");
032        blurEnabled.onChange(v->setBlurEnabled(blurEnabled.selected()));
033        grid.onChange(v->{if(ui()!=null){ui().background().grid(grid.selected());changed();}});
034        animated.onChange(v->{if(ui()!=null){ui().background().animated(animated.selected());changed();}});
035        add(blurEnabled);add(grid);add(animated);
036        templateHeading.id("materialTemplateHeading");add(templateHeading);
037        template("materialMorden",MaterialPreset.MORDEN);
038        template("materialLiquidGlass",MaterialPreset.LIQUID_GLASS);
039        template("materialNormal",MaterialPreset.NORMAL);
040        parameter("materialSigma","模糊 sigma · px",0,32,.5,1,()->theme().sigma,v->{theme().sigma((float)v);if(v>0)rememberedSigma=(float)v;},"物理像素中的高斯标准差。0 表示跳过模糊处理。");
041        parameter("materialBlur","模糊混合比例",0,1,.05,2,()->theme().blur,v->theme().blur((float)v),"0 使用清晰背景;1 使用模糊背景。与 Gaussian sigma 独立。");
042        parameter("materialRefraction","折射强度",0,30,.5,1,()->theme().refraction,v->theme().refraction((float)v),"屏幕空间边缘弯折强度;0..30 全程连续响应,不是物理 IOR。");
043        parameter("materialShine","边缘高光 · 强度",0,1.5,.05,2,()->theme().shine,v->theme().shine((float)v),"0 关闭光学高光;沿实际圆角轮廓受光,含相反方向的环境反射。不会给整块控件叠白色遮罩。");
044        parameter("materialDispersion","RGB 色散 · px",0,2,.05,2,()->theme().dispersion,v->theme().dispersion((float)v),"红/蓝单通道在轮廓处的偏移,单位为逻辑像素。0 关闭;强背景模糊不再把色散一起抹掉。");
045        parameter("materialTint","材质 tint",0,1,.05,2,()->theme().tint,v->theme().tint((float)v),"调整玻璃自身的着色比例,不绘制额外底板。");
046        parameter("materialOpacity","表面不透明度",0,1,.05,2,()->theme().surfaceOpacity,v->theme().opacity((float)v),"默认 1,避免清晰背景从 alpha 再次漏入;不等于背景模糊程度。");
047        parameter("materialSelectedTint","选中 tint 增量",0,1,.05,2,()->theme().selectedTint,v->theme().selectedTint((float)v),"选中时直接增加材质 tint,仍不添加描边。");
048        preset("materialClear","通透",()->{theme().sigma(5).blur(.14f).refraction(18).shine(1.0f).dispersion(1.20f).tint(.10f).opacity(1);rememberedSigma=5;});
049        preset("materialSoft","柔和",()->{theme().sigma(24).blur(1).refraction(10).shine(.65f).dispersion(.65f).tint(.20f).opacity(1);rememberedSigma=24;});
050        preset("materialNeutral","低反光",()->{theme().sigma(18).blur(.98f).refraction(10).shine(.35f).dispersion(.35f).tint(.20f).opacity(1);rememberedSigma=18;});
051        preset("materialReset","恢复默认",this::resetMaterial);
052        note.id("materialNote");add(note);
053    }
054    private void template(String id,MaterialPreset preset){
055        Button b=new Button(preset.displayName());b.id(id);
056        b.tooltip("应用 "+preset.displayName()+" 的全部八项材质参数;保留配色、背景、字体和动画。");
057        b.onClick(v->applyPreset(preset));templates.add(b);add(b);
058    }
059    /** Apply a named material preset and immediately synchronize every numeric editor and slider. */
060    public void applyPreset(MaterialPreset preset){
061        if(preset==null)throw new NullPointerException("preset");
062        preset.applyTo(theme());
063        if(theme().sigma>0)rememberedSigma=theme().sigma;
064        // An explicit preset choice replaces unfinished numeric edits, too. An old
065        // focused editor must not restore stale values when it later loses focus.
066        for(Parameter p:parameters)p.value.number(p.read.getAsDouble());
067        syncFromModel();changed();
068    }
069    private void preset(String id,String title,Runnable action){Button b=new Button(title);b.id(id);b.onClick(v->{action.run();syncFromModel();changed();});presets.add(b);add(b);}
070    private void parameter(String id,String title,double min,double max,double step,int decimals,DoubleSupplier read,DoubleConsumer write,String tip){Parameter p=new Parameter(id,title,min,max,step,decimals,read,write);p.tooltip(tip);parameters.add(p);add(p);}
071    /** Toggle the actual Gaussian pass while preserving the last nonzero sigma. */
072    public void setBlurEnabled(boolean enabled){
073        if(enabled){if(theme().sigma<=0)theme().sigma(rememberedSigma>0?rememberedSigma:18);}
074        else{if(theme().sigma>0)rememberedSigma=theme().sigma;theme().sigma(0);}
075        syncFromModel();changed();
076    }
077    /** Restore only material scalars; palette, corners, typography, background and AA remain unchanged. */
078    public void resetMaterial(){
079        Theme defaults=theme().dark?Theme.dark():Theme.light();
080        theme().sigma(defaults.sigma).blur(defaults.blur).refraction(defaults.refraction).shine(defaults.shine)
081            .dispersion(defaults.dispersion).tint(defaults.tint).opacity(defaults.surfaceOpacity).selectedTint(defaults.selectedTint);
082        rememberedSigma=defaults.sigma;syncFromModel();
083    }
084    /** Pull application-side parameter changes without firing another change event. */
085    public void syncFromModel(){
086        if(theme().sigma>0)rememberedSigma=theme().sigma;
087        blurEnabled.selected(theme().sigma>0);
088        if(ui()!=null){grid.selected(ui().background().grid());animated.selected(ui().background().animated());}
089        for(Parameter p:parameters)p.sync();
090    }
091    private int buttonColumns(float width,int count){
092        // Wrap, rather than squeezing labels or introducing unequal outer insets.
093        return Math.max(1,Math.min(count,(int)((Math.max(0,width)+gap)/(120+gap))));
094    }
095    private float buttonGroupHeight(float width,int count){
096        int columns=buttonColumns(width,count),rows=(count+columns-1)/columns;
097        return rows*BUTTON_HEIGHT+(rows-1)*gap;
098    }
099    private float switchHeight(float width){return width>=590?36:3*36+2*gap;}
100    /** Content height including responsive template/preset rows, matching layout exactly. */
101    public float preferredHeight(float width){
102        int columns=width>=590?2:1,rows=(PARAMETER_COUNT+columns-1)/columns;
103        return switchHeight(width)+gap+TEMPLATE_HEADING_HEIGHT
104            +buttonGroupHeight(width,TEMPLATE_COUNT)+gap
105            +rows*ROW+(rows-1)*gap+gap
106            +buttonGroupHeight(width,PRESET_COUNT)+gap+52;
107    }
108    private float layoutButtons(List<Button> buttons,float y){
109        int columns=buttonColumns(w,buttons.size());float each=Math.max(0,(w-(columns-1)*gap)/columns);
110        for(int i=0;i<buttons.size();i++)buttons.get(i).bounds((i%columns)*(each+gap),y+(i/columns)*(BUTTON_HEIGHT+gap),each,BUTTON_HEIGHT);
111        return y+buttonGroupHeight(w,buttons.size());
112    }
113    @Override protected void layoutChildren(){
114        syncFromModel();int columns=w>=590?2:1;float switchWidth=columns==2?(w-2*gap)/3:w;
115        Switch[] toggles={blurEnabled,grid,animated};for(int i=0;i<toggles.length;i++)toggles[i].bounds(columns==2?i*(switchWidth+gap):0,columns==2?0:i*(36+gap),switchWidth,36);
116        float y=switchHeight(w)+gap;
117        templateHeading.bounds(0,y,w,TEMPLATE_HEADING_HEIGHT);y+=TEMPLATE_HEADING_HEIGHT;
118        y=layoutButtons(templates,y)+gap;
119        float each=(w-(columns-1)*gap)/columns;
120        for(int i=0;i<parameters.size();i++)parameters.get(i).bounds((i%columns)*(each+gap),y+(i/columns)*(ROW+gap),each,ROW);
121        y+=((parameters.size()+columns-1)/columns)*(ROW+gap);
122        y=layoutButtons(presets,y)+gap;
123        note.bounds(0,y,w,52);h=preferredHeight(w);
124    }
125    private final class Parameter extends Panel {
126        final DoubleSupplier read;final DoubleConsumer write;final Slider slider;final Spinner value;final Label label;final double tolerance;
127        Parameter(String id,String title,double min,double max,double step,int decimals,DoubleSupplier read,DoubleConsumer write){
128            this.read=read;this.write=write;this.tolerance=Math.pow(10,-decimals)*.51;
129            glass(false);padding(0);clipChildren(false);
130            label=new Label(title).size(12);add(label);
131            slider=new Slider();slider.range(min,max).continuous();slider.id(id);add(slider);
132            value=new Spinner(min);value.range(min,max).step(step).decimals(decimals);value.id(id+"Value");add(value);
133            slider.onChange(v->{write.accept(slider.value());GlassSettingsPanel.this.changed();});
134            value.onChange(v->{write.accept(value.number());GlassSettingsPanel.this.changed();});
135        }
136        void sync(){double v=read.getAsDouble();slider.value(v);if(!value.focused()&&Math.abs(value.number()-v)>tolerance)value.number(v);}
137        @Override protected void layoutChildren(){float numeric=Math.min(132,w*.47f);label.bounds(0,5,Math.max(0,w-numeric-8),24);value.bounds(w-numeric,0,numeric,32);slider.bounds(0,38,w,32);}
138    }
139}