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}