diff --git a/engine/build.gradle b/engine/build.gradle index 18fad26d7..1bd614902 100644 --- a/engine/build.gradle +++ b/engine/build.gradle @@ -6,6 +6,7 @@ import org.reflections.Reflections; import org.reflections.scanners.SubTypesScanner; import org.reflections.scanners.TypeAnnotationsScanner; import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.ClasspathHelper; // Dependencies needed for what our Gradle scripts themselves use. It cannot be included via an external Gradle file :-( buildscript { @@ -13,12 +14,21 @@ buildscript { // External libs - jcenter is Bintray and is supposed to be a superset of Maven Central, but do both just in case jcenter() mavenCentral() + + // HACK: Needed for NUI reflections + mavenLocal() + google() + maven { url "http://artifactory.terasology.org/artifactory/virtual-repo-live" } } dependencies { // Needed for caching reflected data during builds classpath 'org.reflections:reflections:0.9.10' classpath 'dom4j:dom4j:1.6.1' + + // HACK: Needed for NUI reflections + classpath group: 'org.terasology.nui', name: 'nui', version: '2.0.0-SNAPSHOT' + classpath group: 'javax.servlet', name: 'javax.servlet-api', version: '3.0.1' } } @@ -31,6 +41,8 @@ dependencies { compile(group: 'org.terasology.gestalt', name: 'gestalt-entity-system', version: '7.0.6-SNAPSHOT') compile(group: 'org.terasology.gestalt', name: 'gestalt-module', version: '7.0.6-SNAPSHOT') compile(group: 'org.terasology.gestalt', name: 'gestalt-util', version: '7.0.6-SNAPSHOT') + compile group: 'org.terasology.nui', name: 'nui', version: '2.0.0' + compile group: 'org.terasology.nui', name: 'nui-libgdx', version: '2.0.0' compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.4.0' @@ -64,6 +76,7 @@ task cacheReflections { dirToReflect.mkdirs() Reflections reflections = new Reflections(new ConfigurationBuilder() .addUrls(dirToReflect.toURI().toURL()) + .addUrls(ClasspathHelper.forPackage("org.terasology.nui.widgets")) .setScanners(new TypeAnnotationsScanner(), new SubTypesScanner())) reflections.save(sourceSets.main.output.classesDir.toString() + "/reflections.cache") } diff --git a/engine/src/main/java/org/destinationsol/GameOptions.java b/engine/src/main/java/org/destinationsol/GameOptions.java index 8babf82c0..4f7c8388f 100644 --- a/engine/src/main/java/org/destinationsol/GameOptions.java +++ b/engine/src/main/java/org/destinationsol/GameOptions.java @@ -133,6 +133,7 @@ public Volume advance() { public static final int DEFAULT_BUTTON_RIGHT = -1; public static final int DEFAULT_MAP_SCROLL_SPEED = 10; public static final int DEFAULT_MOBILE_MAP_SCROLL_SPEED = 5; + private static final float DEFAULT_NUI_UI_SCALE = 1.0f; public int x; public int y; @@ -177,6 +178,7 @@ public Volume advance() { private int controllerButtonUp; private int controllerButtonDown; private int mapScrollSpeed; + public float nuiUiScale; private ResolutionProvider resolutionProvider; @@ -226,6 +228,7 @@ public GameOptions(boolean mobile, SolFileReader solFileReader) { controllerButtonDown = reader.getInt("controllerButtonDown", DEFAULT_BUTTON_DOWN); canSellEquippedItems = reader.getBoolean("canSellEquippedItems", false); mapScrollSpeed = reader.getInt("mapScrollSpeed", mobile ? DEFAULT_MOBILE_MAP_SCROLL_SPEED : DEFAULT_MAP_SCROLL_SPEED); + nuiUiScale = reader.getFloat("nuiUiScale", DEFAULT_NUI_UI_SCALE); } public void advanceResolution() { @@ -268,6 +271,13 @@ public void advanceMapScrollSpeed() { } } + public void advanceNuiUiScale() { + nuiUiScale += 0.25f; + if (nuiUiScale > 2.0f) { + nuiUiScale = 0.25f; + } + } + /** * Save the configuration settings to file. */ @@ -286,7 +296,7 @@ public void save() { "controllerButtonShoot2", getControllerButtonShoot2(), "controllerButtonAbility", getControllerButtonAbility(), "controllerButtonLeft", getControllerButtonLeft(), "controllerButtonRight", getControllerButtonRight(), "controllerButtonUp", getControllerButtonUp(), "controllerButtonDown", getControllerButtonDown(), - "mapScrollSpeed", getMapScrollSpeed()); + "mapScrollSpeed", getMapScrollSpeed(), "nuiUiScale", getNuiUiScale()); } /** @@ -1005,4 +1015,12 @@ public int getMapScrollSpeed() { public void setMapScrollSpeed(int mapScrollSpeed) { this.mapScrollSpeed = mapScrollSpeed; } + + public float getNuiUiScale() { + return nuiUiScale; + } + + public void setNuiUiScale(float nuiUiScale) { + this.nuiUiScale = nuiUiScale; + } } diff --git a/engine/src/main/java/org/destinationsol/SolApplication.java b/engine/src/main/java/org/destinationsol/SolApplication.java index b0ded8033..e1b1ecb99 100644 --- a/engine/src/main/java/org/destinationsol/SolApplication.java +++ b/engine/src/main/java/org/destinationsol/SolApplication.java @@ -65,6 +65,7 @@ import org.destinationsol.ui.SolInputManager; import org.destinationsol.ui.SolLayouts; import org.destinationsol.ui.UiDrawer; +import org.destinationsol.ui.nui.NUIManager; import org.destinationsol.util.FramerateLimiter; import org.destinationsol.util.InjectionHelper; import org.slf4j.Logger; @@ -106,6 +107,7 @@ public class SolApplication implements ApplicationListener { private String fatalErrorTrace; private SolGame solGame; private ParameterAdapterManager parameterAdapterManager; + private NUIManager nuiManager; private Context context; // TODO: Make this non-static. public static DisplayDimensions displayDimensions; @@ -162,6 +164,8 @@ public void create() { inputManager.setScreen(this, menuScreens.main); parameterAdapterManager = ParameterAdapterManager.createCore(this); + + nuiManager = new NUIManager(this, commonDrawer, options); } @Override @@ -236,6 +240,7 @@ private void update() { } inputManager.update(this); + nuiManager.update(this); if (solGame != null) { solGame.update(); @@ -288,6 +293,8 @@ private void draw() { } uiDrawer.updateMtx(); inputManager.draw(uiDrawer, this); + nuiManager.draw(commonDrawer); + inputManager.drawCursor(uiDrawer); if (solGame != null) { solGame.drawDebugUi(uiDrawer); @@ -400,6 +407,10 @@ public MenuBackgroundManager getMenuBackgroundManager() { return menuBackgroundManager; } + public NUIManager getNuiManager() { + return nuiManager; + } + // TODO: Make this non-static. public static void addResizeSubscriber(ResizeSubscriber resizeSubscriber) { resizeSubscribers.add(resizeSubscriber); diff --git a/engine/src/main/java/org/destinationsol/assets/AssetHelper.java b/engine/src/main/java/org/destinationsol/assets/AssetHelper.java index 75c485ea5..5a6249690 100644 --- a/engine/src/main/java/org/destinationsol/assets/AssetHelper.java +++ b/engine/src/main/java/org/destinationsol/assets/AssetHelper.java @@ -16,15 +16,18 @@ package org.destinationsol.assets; import org.destinationsol.assets.music.AndroidOggMusicFileFormat; -import org.destinationsol.assets.music.OggMusic; -import org.destinationsol.assets.music.OggMusicData; import org.destinationsol.assets.sound.AndroidOggSoundFileFormat; -import org.destinationsol.assets.sound.OggSound; import org.destinationsol.assets.sound.OggSoundData; +import org.destinationsol.assets.music.OggMusicData; +import org.destinationsol.assets.music.OggMusic; +import org.destinationsol.assets.sound.OggSound; +import org.destinationsol.assets.ui.UIFormat; +import org.destinationsol.assets.ui.UISkinFormat; import org.terasology.gestalt.assets.Asset; import org.terasology.gestalt.assets.AssetData; import org.terasology.gestalt.assets.AssetType; import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.gestalt.assets.format.producer.AssetFileDataProducer; import org.terasology.gestalt.assets.module.ModuleAwareAssetTypeManager; import org.terasology.gestalt.assets.module.ModuleAwareAssetTypeManagerImpl; import org.terasology.gestalt.assets.module.ModuleDependencyResolutionStrategy; @@ -35,6 +38,13 @@ import org.terasology.gestalt.entitysystem.prefab.PrefabJsonFormat; import org.terasology.gestalt.module.ModuleEnvironment; import org.terasology.gestalt.naming.Name; +import org.terasology.nui.UIWidget; +import org.terasology.nui.asset.UIElement; +import org.terasology.nui.reflection.WidgetLibrary; +import org.terasology.nui.skin.UISkin; +import org.terasology.reflection.copy.CopyStrategyLibrary; +import org.terasology.reflection.reflect.ReflectFactory; +import org.terasology.reflection.reflect.ReflectionReflectFactory; import java.util.Optional; import java.util.Set; @@ -63,6 +73,20 @@ public void init(ModuleEnvironment environment, ComponentManager componentManage new ModuleEnvironmentDependencyProvider(environment))), componentManager, assetTypeManager.getAssetManager()).create()); + // The NUI widgets are loaded here so that they can be found when reading the UI JSON files (in UIFormat.UIWidgetTypeAdapter) + ReflectFactory reflectFactory = new ReflectionReflectFactory(); + WidgetLibrary widgetLibrary = new WidgetLibrary(environment, reflectFactory, new CopyStrategyLibrary(reflectFactory)); + for (Class widgetClass : environment.getSubtypesOf(UIWidget.class)) { + Name moduleName = environment.getModuleProviding(widgetClass); + widgetLibrary.register(new ResourceUrn(moduleName, new Name(widgetClass.getSimpleName())), widgetClass); + } + + assetTypeManager.createAssetType(UISkin.class, UISkin::new, "skins"); + ((AssetFileDataProducer)assetTypeManager.getAssetType(UISkin.class).get().getProducers().get(0)).addAssetFormat(new UISkinFormat(widgetLibrary)); + + assetTypeManager.createAssetType(UIElement.class, UIElement::new, "ui"); + ((AssetFileDataProducer)assetTypeManager.getAssetType(UIElement.class).get().getProducers().get(0)).addAssetFormat(new UIFormat(widgetLibrary)); + assetTypeManager.switchEnvironment(environment); } diff --git a/engine/src/main/java/org/destinationsol/assets/fonts/FontData.java b/engine/src/main/java/org/destinationsol/assets/fonts/FontData.java index 9656d8915..d6b63e87c 100644 --- a/engine/src/main/java/org/destinationsol/assets/fonts/FontData.java +++ b/engine/src/main/java/org/destinationsol/assets/fonts/FontData.java @@ -17,11 +17,13 @@ import com.badlogic.gdx.graphics.g2d.BitmapFont; import org.terasology.gestalt.assets.AssetData; +import org.terasology.nui.backends.libgdx.LibGDXFont; -public class FontData implements AssetData { +public class FontData extends LibGDXFont implements AssetData { private BitmapFont bitmapFont; public FontData(BitmapFont bitmapFont) { + super(bitmapFont); this.bitmapFont = bitmapFont; } diff --git a/engine/src/main/java/org/destinationsol/assets/fonts/FontFileFormat.java b/engine/src/main/java/org/destinationsol/assets/fonts/FontFileFormat.java index d3e2fd88b..225b08d2b 100644 --- a/engine/src/main/java/org/destinationsol/assets/fonts/FontFileFormat.java +++ b/engine/src/main/java/org/destinationsol/assets/fonts/FontFileFormat.java @@ -15,8 +15,10 @@ */ package org.destinationsol.assets.fonts; +import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.utils.Array; import org.destinationsol.assets.AssetDataFileHandle; import org.destinationsol.assets.Assets; import org.terasology.gestalt.assets.ResourceUrn; @@ -38,12 +40,17 @@ public FontData load(ResourceUrn urn, List inputs) throws IOExcep AssetDataFileHandle fontDataHandle = new AssetDataFileHandle(inputs.get(0)); BitmapFont.BitmapFontData fontData = new BitmapFont.BitmapFontData(fontDataHandle, true); - String[] fontTexturePath = fontData.imagePaths[0].split("/"); - String fontTextureName = fontTexturePath[fontTexturePath.length - 1]; - fontTextureName = fontTextureName.substring(0, fontTextureName.lastIndexOf('.')); - TextureRegion fontTexture = new TextureRegion(Assets.getDSTexture(urn.getModuleName() + ":" + fontTextureName).getTexture()); + TextureRegion[] fontTextures = new TextureRegion[fontData.imagePaths.length]; + for (int textureNo = 0; textureNo < fontData.imagePaths.length; textureNo++){ + String[] pathSegments = fontData.imagePaths[textureNo].split("/"); + String fontTextureName = pathSegments[pathSegments.length - 1]; + fontTextureName = fontTextureName.substring(0, fontTextureName.lastIndexOf('.')); + Texture texture = Assets.getDSTexture(urn.getModuleName() + ":" + fontTextureName).getTexture(); + TextureRegion fontTexture = new TextureRegion(texture); + fontTextures[textureNo] = fontTexture; + } - BitmapFont bitmapFont = new BitmapFont(fontData, fontTexture, false); + BitmapFont bitmapFont = new BitmapFont(fontData, Array.with(fontTextures), false); bitmapFont.setUseIntegerPositions(false); return new FontData(bitmapFont); } diff --git a/engine/src/main/java/org/destinationsol/assets/fonts/UIFont.java b/engine/src/main/java/org/destinationsol/assets/fonts/UIFont.java new file mode 100644 index 000000000..ac1746086 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/assets/fonts/UIFont.java @@ -0,0 +1,114 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.assets.fonts; + +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import org.joml.Vector2i; +import org.terasology.nui.backends.libgdx.LibGDXFont; + +import java.util.List; + +/** + * This class wraps a {@link Font} asset in order to adapt it to NUI's LibGDX back-end. It handles font scaling, + * so that NUI renders correctly. + */ +public class UIFont extends LibGDXFont { + private float fontScale; + private float previousFontScaleX; + private float previousFontScaleY; + private static final float FONT_SCALE = 8.0f; + + public UIFont(Font font) { + super(font.getBitmapFont()); + + fontScale = FONT_SCALE / super.getGdxFont().getData().xHeight; + } + + private void scale() { + scale(fontScale, fontScale); + } + + private void scale(float scaleX, float scaleY) { + previousFontScaleX = super.getGdxFont().getScaleX(); + previousFontScaleY = super.getGdxFont().getScaleY(); + super.getGdxFont().getData().setScale(scaleX, scaleY); + } + + private void reset() { + super.getGdxFont().getData().setScale(previousFontScaleX, previousFontScaleY); + } + + @Override + public Vector2i getSize(List lines) { + scale(); + Vector2i result = super.getSize(lines); + reset(); + return result; + } + + @Override + public int getWidth(Character c) { + scale(); + int result = super.getWidth(c); + reset(); + return result; + } + + @Override + public int getLineHeight() { + scale(); + int result = super.getLineHeight(); + reset(); + return result; + } + + @Override + public int getHeight(String text) { + scale(); + int result = super.getHeight(text); + reset(); + return result; + } + + @Override + public int getBaseHeight() { + scale(); + int result = super.getBaseHeight(); + reset(); + return result; + } + + @Override + public int getWidth(String text) { + scale(); + int result = super.getWidth(text); + reset(); + return result; + } + + @Override + public BitmapFont getGdxFont() { + super.getGdxFont().getData().setScale(fontScale, -fontScale); + return super.getGdxFont(); + } + + @Override + public GlyphLayout getGlyphLayout() { + super.getGdxFont().getData().setScale(fontScale, -fontScale); + return super.getGlyphLayout(); + } +} diff --git a/engine/src/main/java/org/destinationsol/assets/sound/PlayableSound.java b/engine/src/main/java/org/destinationsol/assets/sound/PlayableSound.java index f5df0451e..3ac286bd4 100644 --- a/engine/src/main/java/org/destinationsol/assets/sound/PlayableSound.java +++ b/engine/src/main/java/org/destinationsol/assets/sound/PlayableSound.java @@ -17,10 +17,12 @@ import org.destinationsol.assets.sound.OggSound; +import org.terasology.nui.asset.Sound; + /** * A class that stores an OggSound or a set of OggSounds. */ -public interface PlayableSound { +public interface PlayableSound extends Sound { /** * Returns an OggSound selected from the sound set. @@ -35,4 +37,9 @@ public interface PlayableSound { * @return */ float getBasePitch(); + + @Override + default void play(float volume) { + getOggSound().getSound().play(volume, getBasePitch(), 0); + } } diff --git a/engine/src/main/java/org/destinationsol/assets/textures/DSTexture.java b/engine/src/main/java/org/destinationsol/assets/textures/DSTexture.java index 53492e599..21356c29d 100644 --- a/engine/src/main/java/org/destinationsol/assets/textures/DSTexture.java +++ b/engine/src/main/java/org/destinationsol/assets/textures/DSTexture.java @@ -20,6 +20,7 @@ import org.terasology.gestalt.assets.AssetType; import org.terasology.gestalt.assets.ResourceUrn; import org.terasology.gestalt.assets.module.annotations.RegisterAssetType; +import org.terasology.nui.UITextureRegion; @RegisterAssetType(folderName = {"textures", "ships", "items", "grounds", "mazes", "asteroids", "fonts"}, factoryClass = DSTextureFactory.class) public class DSTexture extends Asset { @@ -38,4 +39,12 @@ protected void doReload(DSTextureData data) { public Texture getTexture() { return dsTextureData.getTexture(); } + + /** + * Obtains the NUI {@link UITextureRegion} associated with this texture. + * @return the assoicated UI texture + */ + public UITextureRegion getUiTexture() { + return dsTextureData; + } } diff --git a/engine/src/main/java/org/destinationsol/assets/textures/DSTextureData.java b/engine/src/main/java/org/destinationsol/assets/textures/DSTextureData.java index 5619e8e45..069c05d56 100644 --- a/engine/src/main/java/org/destinationsol/assets/textures/DSTextureData.java +++ b/engine/src/main/java/org/destinationsol/assets/textures/DSTextureData.java @@ -16,12 +16,15 @@ package org.destinationsol.assets.textures; import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.TextureRegion; import org.terasology.gestalt.assets.AssetData; +import org.terasology.nui.backends.libgdx.LibGDXTexture; -public class DSTextureData implements AssetData { +public class DSTextureData extends LibGDXTexture implements AssetData { private Texture texture; public DSTextureData(Texture texture) { + super(new TextureRegion(texture)); this.texture = texture; } diff --git a/engine/src/main/java/org/destinationsol/assets/ui/CaseInsensitiveEnumTypeAdapterFactory.java b/engine/src/main/java/org/destinationsol/assets/ui/CaseInsensitiveEnumTypeAdapterFactory.java new file mode 100644 index 000000000..1e8b5dea7 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/assets/ui/CaseInsensitiveEnumTypeAdapterFactory.java @@ -0,0 +1,77 @@ +/* + * Copyright 2013 MovingBlocks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.assets.ui; + +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.Locale; +import java.util.Map; + +/** + * A Gson Adapter factory for supporting enums in a case-insensitive manner + * NOTE: Taken from Terasology for compatibility with NUI JSON files. + */ +public class CaseInsensitiveEnumTypeAdapterFactory implements TypeAdapterFactory { + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + Class rawType = (Class) type.getRawType(); + if (!rawType.isEnum()) { + return null; + } + + final Map lowercaseToConstant = Maps.newHashMap(); + for (T constant : rawType.getEnumConstants()) { + String norm = normalize(constant.toString()); + lowercaseToConstant.put(norm, constant); + } + + return new TypeAdapter() { + @Override + public void write(JsonWriter out, T value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(normalize(value.toString())); + } + } + + @Override + public T read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } else { + String value = reader.nextString(); + return lowercaseToConstant.get(normalize(value)); + } + } + }; + } + + private String normalize(String name) { + return name.toLowerCase(Locale.ENGLISH); + } +} + diff --git a/engine/src/main/java/org/destinationsol/assets/ui/UIFormat.java b/engine/src/main/java/org/destinationsol/assets/ui/UIFormat.java new file mode 100644 index 000000000..2e03b1a4e --- /dev/null +++ b/engine/src/main/java/org/destinationsol/assets/ui/UIFormat.java @@ -0,0 +1,247 @@ +/* + * Copyright 2016 MovingBlocks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.assets.ui; + +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonReader; +import org.destinationsol.assets.Assets; +import org.joml.Vector2i; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.gestalt.assets.format.AbstractAssetFileFormat; +import org.terasology.gestalt.assets.format.AssetDataFile; +import org.terasology.gestalt.assets.management.AssetManager; +import org.terasology.gestalt.assets.module.annotations.RegisterAssetFileFormat; +import org.terasology.nui.Color; +import org.terasology.nui.LayoutHint; +import org.terasology.nui.UILayout; +import org.terasology.nui.UITextureRegion; +import org.terasology.nui.UIWidget; +import org.terasology.nui.asset.UIData; +import org.terasology.nui.asset.font.Font; +import org.terasology.nui.reflection.WidgetLibrary; +import org.terasology.nui.skin.UISkin; +import org.terasology.nui.widgets.UILabel; +import org.terasology.reflection.ReflectionUtil; +import org.terasology.reflection.metadata.ClassMetadata; +import org.terasology.reflection.metadata.FieldMetadata; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; + +/** + * Handles loading UI widgets from json format files. + * NOTE: Taken from Terasology for compatibility with NUI JSON files. + */ +@RegisterAssetFileFormat +public class UIFormat extends AbstractAssetFileFormat { + + public static final String CONTENTS_FIELD = "contents"; + public static final String LAYOUT_INFO_FIELD = "layoutInfo"; + public static final String ID_FIELD = "id"; + public static final String TYPE_FIELD = "type"; + + private static final Logger logger = LoggerFactory.getLogger(UIFormat.class); + private static WidgetLibrary library; + + public UIFormat(WidgetLibrary library) { + super("ui"); + + UIFormat.library = library; + } + + @Override + public UIData load(ResourceUrn resourceUrn, List inputs) throws IOException { + try (JsonReader reader = new JsonReader(new InputStreamReader(inputs.get(0).openStream(), Charsets.UTF_8))) { + reader.setLenient(true); + UIData data = load(new JsonParser().parse(reader)); + data.setSource(inputs.get(0)); + return data; + } + } + + public UIData load(JsonElement element) throws IOException { + return load(element, null); + } + + public UIData load(JsonElement element, Locale otherLocale) throws IOException { + GsonBuilder gsonBuilder = new GsonBuilder() + .registerTypeAdapterFactory(new CaseInsensitiveEnumTypeAdapterFactory()) + .registerTypeAdapter(UIData.class, new UIDataTypeAdapter()) + .registerTypeAdapter(UISkin.class, new JsonDeserializer() { + @Override + public UISkin deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return Assets.getAssetHelper().get(new ResourceUrn(json.getAsString()), UISkin.class).get(); + } + }) + .registerTypeAdapter(UITextureRegion.class, new UISkinFormat.TextureRegionTypeAdapter()) + .registerTypeAdapter(Optional.class, new UISkinFormat.OptionalTextureRegionTypeAdapter()) + .registerTypeAdapter(Font.class, new UISkinFormat.FontTypeAdapter()) + .registerTypeAdapter(Color.class, new UISkinFormat.ColorTypeHandler()) + .registerTypeAdapter(Vector2i.class, new Vector2iTypeAdaptor()) + .registerTypeHierarchyAdapter(UIWidget.class, new UIWidgetTypeAdapter()); + Gson gson = gsonBuilder.create(); + return gson.fromJson(element, UIData.class); + } + + /** + * Load UIData with a single, root widget + */ + private static final class UIDataTypeAdapter implements JsonDeserializer { + + @Override + public UIData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return new UIData((UIWidget) context.deserialize(json, UIWidget.class)); + } + } + + private static final class Vector2iTypeAdaptor implements JsonDeserializer { + + @Override + public Vector2i deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonArray array = json.getAsJsonArray(); + return new Vector2i(array.get(0).getAsInt(), array.get(1).getAsInt()); + } + } + + /** + * Loads a widget. This requires the following custom handling: + *
    + *
  • The class of the widget is determined through a URI in the "type" attribute
  • + *
  • If the "id" attribute is present, it is passed to the constructor
  • + *
  • If the widget is a layout, then a "contents" attribute provides a list of widgets for content. + * Each contained widget may have a "layoutInfo" attribute providing the layout hint for its container.
  • + *
+ */ + private static final class UIWidgetTypeAdapter implements JsonDeserializer { + @Override + public UIWidget deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { + return new UILabel(json.getAsString()); + } + + JsonObject jsonObject = json.getAsJsonObject(); + + String type = jsonObject.get(TYPE_FIELD).getAsString(); + ClassMetadata elementMetadata = library.resolve(type); + if (elementMetadata == null) { + logger.error("Unknown UIWidget type {}", type); + return null; + } + + String id = null; + if (jsonObject.has(ID_FIELD)) { + id = jsonObject.get(ID_FIELD).getAsString(); + } + + UIWidget element = elementMetadata.newInstance(); + if (id != null) { + FieldMetadata fieldMetadata = elementMetadata.getField(ID_FIELD); + if (fieldMetadata == null) { + logger.warn("UIWidget type {} lacks id field", elementMetadata.getUri()); + } else { + fieldMetadata.setValue(element, id); + } + } + + // Deserialize normal fields. + Set unknownFields = new HashSet<>(); + for (Entry entry : jsonObject.entrySet()) { + String name = entry.getKey(); + if (!ID_FIELD.equals(name) + && !CONTENTS_FIELD.equals(name) + && !TYPE_FIELD.equals(name) + && !LAYOUT_INFO_FIELD.equals(name)) { + unknownFields.add(name); + } + } + + for (FieldMetadata field : elementMetadata.getFields()) { + if (jsonObject.has(field.getSerializationName())) { + unknownFields.remove(field.getSerializationName()); + if (field.getName().equals(CONTENTS_FIELD) && UILayout.class.isAssignableFrom(elementMetadata.getType())) { + continue; + } + try { + if (List.class.isAssignableFrom(field.getType())) { + Type contentType = ReflectionUtil.getTypeParameter(field.getField().getGenericType(), 0); + if (contentType != null) { + List result = Lists.newArrayList(); + JsonArray list = jsonObject.getAsJsonArray(field.getSerializationName()); + for (JsonElement item : list) { + result.add(context.deserialize(item, contentType)); + } + field.setValue(element, result); + } + } else { + field.setValue(element, context.deserialize(jsonObject.get(field.getSerializationName()), field.getType())); + } + } catch (RuntimeException e) { + logger.error("Failed to deserialize field {} of {}", field.getName(), type, e); + } + } + } + + for (String key : unknownFields) { + logger.warn("Field '{}' not recognized for {} in {}", key, typeOfT, json); + } + + // Deserialize contents and layout hints + if (UILayout.class.isAssignableFrom(elementMetadata.getType())) { + UILayout layout = (UILayout) element; + + Class layoutHintType = (Class) + ReflectionUtil.getTypeParameter(elementMetadata.getType().getGenericSuperclass(), 0); + if (jsonObject.has(CONTENTS_FIELD)) { + for (JsonElement child : jsonObject.getAsJsonArray(CONTENTS_FIELD)) { + UIWidget childElement = context.deserialize(child, UIWidget.class); + if (childElement != null) { + LayoutHint hint = null; + if (child.isJsonObject()) { + JsonObject childObject = child.getAsJsonObject(); + if (layoutHintType != null && !layoutHintType.isInterface() && !Modifier.isAbstract(layoutHintType.getModifiers()) + && childObject.has(LAYOUT_INFO_FIELD)) { + hint = context.deserialize(childObject.get(LAYOUT_INFO_FIELD), layoutHintType); + } + } + layout.addWidget(childElement, hint); + } + } + } + } + return element; + } + } +} diff --git a/engine/src/main/java/org/destinationsol/assets/ui/UISkinFormat.java b/engine/src/main/java/org/destinationsol/assets/ui/UISkinFormat.java new file mode 100644 index 000000000..1d58503a5 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/assets/ui/UISkinFormat.java @@ -0,0 +1,236 @@ +package org.destinationsol.assets.ui; + +import com.google.common.base.Charsets; +import com.google.common.primitives.UnsignedInts; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.stream.JsonReader; +import org.destinationsol.assets.Assets; +import org.destinationsol.assets.fonts.UIFont; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.gestalt.assets.format.AbstractAssetFileFormat; +import org.terasology.gestalt.assets.format.AssetDataFile; +import org.terasology.gestalt.assets.module.annotations.RegisterAssetFileFormat; +import org.terasology.nui.Color; +import org.terasology.nui.UITextureRegion; +import org.terasology.nui.UIWidget; +import org.terasology.nui.asset.font.Font; +import org.terasology.nui.skin.UISkin; +import org.terasology.nui.skin.UISkinBuilder; +import org.terasology.nui.skin.UISkinData; +import org.terasology.nui.skin.UIStyleFragment; +import org.terasology.reflection.metadata.ClassLibrary; +import org.terasology.reflection.metadata.ClassMetadata; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * NOTE: Taken from Terasology for compatibility with NUI JSON files. + */ +@RegisterAssetFileFormat +public class UISkinFormat extends AbstractAssetFileFormat { + + private static final Logger logger = LoggerFactory.getLogger(UISkinFormat.class); + private Gson gson; + private static ClassLibrary widgetClassLibrary; + + public UISkinFormat(ClassLibrary widgetClassLibrary) { + super("skin"); + gson = new GsonBuilder() + .registerTypeAdapter(UISkinData.class, new UISkinTypeAdapter()) + .registerTypeAdapterFactory(new CaseInsensitiveEnumTypeAdapterFactory()) + .registerTypeAdapter(UITextureRegion.class, new TextureRegionTypeAdapter()) + .registerTypeAdapter(Optional.class, new OptionalTextureRegionTypeAdapter()) + .registerTypeAdapter(Font.class, new FontTypeAdapter()) + .registerTypeAdapter(Color.class, new ColorTypeHandler()) + .enableComplexMapKeySerialization() + .serializeNulls() + .create(); + UISkinFormat.widgetClassLibrary = widgetClassLibrary; + } + + @Override + public UISkinData load(ResourceUrn urn, List inputs) throws IOException { + try (JsonReader reader = new JsonReader(new InputStreamReader(inputs.get(0).openStream(), Charsets.UTF_8))) { + reader.setLenient(true); + UISkinData data = gson.fromJson(reader, UISkinData.class); + data.setSource(inputs.get(0)); + return data; + } catch (JsonSyntaxException e) { + e.printStackTrace(); + throw new IOException("Failed to load skin '" + urn + "'", e); + } + } + + public UISkinData load(JsonElement element) throws IOException { + return gson.fromJson(element, UISkinData.class); + } + + public static class TextureRegionTypeAdapter implements JsonDeserializer { + + @Override + public UITextureRegion deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + String uri = json.getAsString(); + return Assets.getDSTexture(uri).getUiTexture(); + } + } + + private static class UISkinTypeAdapter implements JsonDeserializer { + @Override + public UISkinData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonObject()) { + UISkinBuilder builder = new UISkinBuilder(); + DefaultInfo defaultInfo = null; + defaultInfo = context.deserialize(json, DefaultInfo.class); + defaultInfo.apply(builder); + return builder.build(); + } + return null; + } + } + + private static class DefaultInfo extends FamilyInfo { + public String inherit; + public Map families; + + @Override + public void apply(UISkinBuilder builder) { + super.apply(builder); + if (inherit != null) { + Optional skin = Assets.getAssetHelper().get(new ResourceUrn(inherit), UISkin.class); + if (skin.isPresent()) { + builder.setBaseSkin(skin.get()); + } + } + if (families != null) { + for (Map.Entry entry : families.entrySet()) { + builder.setFamily(entry.getKey()); + entry.getValue().apply(builder); + } + } + } + } + + private static class FamilyInfo extends StyleInfo { + public Map elements; + + public void apply(UISkinBuilder builder) { + super.apply(builder); + if (elements != null) { + for (Map.Entry entry : elements.entrySet()) { + ClassLibrary library = widgetClassLibrary; + ClassMetadata metadata = library.resolve(entry.getKey()); + if (metadata != null) { + builder.setElementClass(metadata.getType()); + entry.getValue().apply(builder); + } else { + logger.warn("Failed to resolve UIWidget class {}, skipping style information", entry.getKey()); + } + + } + } + } + } + + private static class PartsInfo extends StyleInfo { + public Map modes; + + public void apply(UISkinBuilder builder) { + super.apply(builder); + if (modes != null) { + for (Map.Entry entry : modes.entrySet()) { + builder.setElementMode(entry.getKey()); + entry.getValue().apply(builder); + } + } + } + } + + private static class ElementInfo extends StyleInfo { + public Map parts; + public Map modes; + + public void apply(UISkinBuilder builder) { + super.apply(builder); + if (modes != null) { + for (Map.Entry entry : modes.entrySet()) { + builder.setElementMode(entry.getKey()); + entry.getValue().apply(builder); + } + } + if (parts != null) { + for (Map.Entry entry : parts.entrySet()) { + builder.setElementPart(entry.getKey()); + entry.getValue().apply(builder); + } + } + } + } + + private static class StyleInfo extends UIStyleFragment { + + private void apply(UISkinBuilder builder) { + builder.setStyleFragment(this); + } + } + + public static class OptionalTextureRegionTypeAdapter implements JsonDeserializer> { + @Override + public Optional deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + String name = json.getAsString(); + if (name.isEmpty()) { + return Optional.empty(); + } + + if (!name.contains(":")) { + name = "engine:" + name; + } + return Optional.of(Assets.getDSTexture(name).getUiTexture()); + } + } + + public static class FontTypeAdapter implements JsonDeserializer { + @Override + public Font deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + String name = json.getAsString(); + if (!name.contains(":")) { + name = "engine:" + name; + } + return new UIFont(Assets.getFont(name)); + } + } + + /** + * Serializes {@link Color} instances to an int array [r, g, b, a]. + * De-serializing also supports hexadecimal strings such as "AAAAAAFF". + */ + public static class ColorTypeHandler implements JsonDeserializer { + @Override + public Color deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonArray()) { + JsonArray array = json.getAsJsonArray(); + return new Color(array.get(0).getAsFloat(), array.get(1).getAsFloat(), array.get(2).getAsFloat(), array.get(3).getAsFloat()); + } + if (json.isJsonPrimitive()) { + // NOTE: Integer.parseUnsignedInt is not available on Android API 24 (7.0). + // Since we still have Guava, we use its equivalent. + return new Color(UnsignedInts.parseUnsignedInt(json.getAsString(), 16)); + } + + return null; + } + } +} diff --git a/engine/src/main/java/org/destinationsol/game/console/commands/ShowNUIScreenCommandHandler.java b/engine/src/main/java/org/destinationsol/game/console/commands/ShowNUIScreenCommandHandler.java new file mode 100644 index 000000000..02c9f7912 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/game/console/commands/ShowNUIScreenCommandHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.game.console.commands; + +import org.destinationsol.assets.Assets; +import org.destinationsol.game.SolGame; +import org.destinationsol.game.console.annotations.Command; +import org.destinationsol.game.console.annotations.CommandParam; +import org.destinationsol.game.console.annotations.Game; +import org.destinationsol.game.console.annotations.RegisterCommands; +import org.destinationsol.game.console.suggesters.NUIScreenSuggester; +import org.destinationsol.ui.nui.NUIManager; +import org.destinationsol.ui.nui.NUIScreenLayer; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.nui.asset.UIElement; + +/** + * A command used to display a particular NUI screen + */ +@RegisterCommands +public class ShowNUIScreenCommandHandler { + @Command(shortDescription = "Displays a NUI screen") + public String showNUIScreen(@Game SolGame game, @CommandParam(value = "screen", suggester = NUIScreenSuggester.class) String screen) { + NUIManager nuiManager = game.getSolApplication().getNuiManager(); + nuiManager.pushScreen((NUIScreenLayer) Assets.getAssetHelper().get(new ResourceUrn(screen), UIElement.class).get().getRootWidget()); + return "Screen displayed."; + } +} diff --git a/engine/src/main/java/org/destinationsol/game/console/suggesters/NUIScreenSuggester.java b/engine/src/main/java/org/destinationsol/game/console/suggesters/NUIScreenSuggester.java new file mode 100644 index 000000000..8307af9b4 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/game/console/suggesters/NUIScreenSuggester.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.game.console.suggesters; + +import org.destinationsol.assets.Assets; +import org.destinationsol.game.SolGame; +import org.destinationsol.game.console.CommandParameterSuggester; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.nui.asset.UIElement; + +import java.util.HashSet; +import java.util.Set; + +public class NUIScreenSuggester implements CommandParameterSuggester { + @Override + public Set suggest(SolGame game, Object... resolvedParameters) { + Set suggestions = new HashSet<>(); + for (ResourceUrn urn : Assets.getAssetHelper().list(UIElement.class)) { + suggestions.add(urn.toString()); + } + + return suggestions; + } +} diff --git a/engine/src/main/java/org/destinationsol/game/screens/MainGameScreen.java b/engine/src/main/java/org/destinationsol/game/screens/MainGameScreen.java index 0876180d2..f013c2dbf 100644 --- a/engine/src/main/java/org/destinationsol/game/screens/MainGameScreen.java +++ b/engine/src/main/java/org/destinationsol/game/screens/MainGameScreen.java @@ -49,6 +49,9 @@ import org.destinationsol.ui.SolUiControl; import org.destinationsol.ui.SolUiScreen; import org.destinationsol.ui.UiDrawer; +import org.destinationsol.ui.nui.screens.ConsoleScreen; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.nui.asset.UIElement; import java.util.ArrayList; import java.util.List; @@ -96,6 +99,7 @@ public class MainGameScreen extends SolUiBaseScreen { private final TextPlace myChargesExcessTp; private final TextPlace myMoneyExcessTp; private final SolApplication solApplication; + private final ConsoleScreen consoleScreen; private List gameOverlayScreens = new ArrayList<>(); private List warnDrawers = new ArrayList<>(); @@ -174,6 +178,8 @@ public class MainGameScreen extends SolUiBaseScreen { compassTexture = Assets.getAtlasRegion("engine:uiCompass"); myCompassTint = SolColor.col(1, 0); + consoleScreen = (ConsoleScreen) Assets.getAssetHelper().get(new ResourceUrn("engine:console"), UIElement.class).get().getRootWidget(); + myLifeTp = new TextPlace(SolColor.W50); myRepairsExcessTp = new TextPlace(SolColor.WHITE); myShieldLifeTp = new TextPlace(SolColor.W50); @@ -242,7 +248,6 @@ public void updateCustom(SolApplication solApplication, SolInputManager.InputPoi if (menuControl.isJustOff()) { inputMan.setScreen(solApplication, screens.menuScreen); } - boolean controlsEnabled = inputMan.getTopScreen() == this; shipControl.update(solApplication, controlsEnabled); @@ -289,12 +294,30 @@ public void updateCustom(SolApplication solApplication, SolInputManager.InputPoi updateTalk(game); + if (solApplication.getNuiManager().hasScreen(consoleScreen)) { + controls.forEach(x -> x.setEnabled(false)); + consoleControlGrave.setEnabled(true); + consoleControlF1.setEnabled(true); + } + if (pauseControl.isJustOff()) { game.setPaused(!game.isPaused()); } + if (consoleScreen.isConsoleJustClosed()) { + game.setPaused(false); + controls.forEach(x -> x.setEnabled(true)); + consoleControlGrave.setEnabled(true); + consoleControlF1.setEnabled(true); + } + if (consoleControlGrave.isJustOff() || consoleControlF1.isJustOff()) { - inputMan.setScreen(solApplication, screens.consoleScreen); + if (solApplication.getNuiManager().hasScreen(consoleScreen)) { + solApplication.getNuiManager().removeScreen(consoleScreen); + } else { + solApplication.getNuiManager().pushScreen(consoleScreen); + game.setPaused(true); + } } for (SolUiScreen screen : gameOverlayScreens) { diff --git a/engine/src/main/java/org/destinationsol/menu/ResolutionScreen.java b/engine/src/main/java/org/destinationsol/menu/ResolutionScreen.java index caeef26d5..70fe9bef8 100644 --- a/engine/src/main/java/org/destinationsol/menu/ResolutionScreen.java +++ b/engine/src/main/java/org/destinationsol/menu/ResolutionScreen.java @@ -41,18 +41,23 @@ public class ResolutionScreen extends SolUiBaseScreen { private final SolUiControl closeControl; private final SolUiControl resolutionControl; private final SolUiControl fullscreenControl; + private final SolUiControl nuiUIScaleControl; ResolutionScreen(MenuLayout menuLayout, GameOptions gameOptions) { displayDimensions = SolApplication.displayDimensions; - resolutionControl = new SolUiControl(menuLayout.buttonRect(-1, 2), true); + resolutionControl = new SolUiControl(menuLayout.buttonRect(-1, 1), true); resolutionControl.setDisplayName("Resolution"); controls.add(resolutionControl); - fullscreenControl = new SolUiControl(menuLayout.buttonRect(-1, 3), true); + fullscreenControl = new SolUiControl(menuLayout.buttonRect(-1, 2), true); fullscreenControl.setDisplayName("Fullscreen"); controls.add(fullscreenControl); + nuiUIScaleControl = new SolUiControl(menuLayout.buttonRect(-1, 3), true); + nuiUIScaleControl.setDisplayName("NUI UI scale"); + controls.add(nuiUIScaleControl); + closeControl = new SolUiControl(menuLayout.buttonRect(-1, 4), true, gameOptions.getKeyEscape()); closeControl.setDisplayName("Back"); controls.add(closeControl); @@ -79,11 +84,13 @@ public void updateCustom(SolApplication solApplication, SolInputManager.InputPoi } if (mode != null) { Gdx.graphics.setFullscreenMode(mode); + solApplication.getNuiManager().resize(mode.width, mode.height); } else { logger.warn("The resolution {}x{} is not supported in fullscreen mode!", options.x, options.y); } } else { Gdx.graphics.setWindowedMode(options.x, options.y); + solApplication.getNuiManager().resize(options.x, options.y); } inputManager.setScreen(solApplication, solApplication.getMenuScreens().options); return; @@ -99,6 +106,12 @@ public void updateCustom(SolApplication solApplication, SolInputManager.InputPoi options.advanceFullscreen(); } + nuiUIScaleControl.setDisplayName("NUI UI scale: " + options.getNuiUiScale()); + if (nuiUIScaleControl.isJustOff()) { + options.advanceNuiUiScale(); + solApplication.getNuiManager().setUiScale(options.getNuiUiScale()); + } + solApplication.getMenuBackgroundManager().update(); } diff --git a/engine/src/main/java/org/destinationsol/modules/ModuleManager.java b/engine/src/main/java/org/destinationsol/modules/ModuleManager.java index c27861b0e..d466bf7c3 100644 --- a/engine/src/main/java/org/destinationsol/modules/ModuleManager.java +++ b/engine/src/main/java/org/destinationsol/modules/ModuleManager.java @@ -16,6 +16,7 @@ package org.destinationsol.modules; import com.google.common.collect.Sets; +import com.google.common.reflect.Reflection; import org.destinationsol.assets.AssetHelper; import org.destinationsol.assets.Assets; import org.destinationsol.assets.music.OggMusic; @@ -34,19 +35,25 @@ import org.terasology.gestalt.module.Module; import org.terasology.gestalt.module.ModuleEnvironment; import org.terasology.gestalt.module.ModuleFactory; +import org.terasology.gestalt.module.ModuleMetadata; import org.terasology.gestalt.module.ModulePathScanner; import org.terasology.gestalt.module.ModuleRegistry; import org.terasology.gestalt.module.TableModuleRegistry; +import org.terasology.gestalt.module.resources.EmptyFileSource; import org.terasology.gestalt.module.sandbox.APIScanner; import org.terasology.gestalt.module.sandbox.ModuleSecurityManager; import org.terasology.gestalt.module.sandbox.ModuleSecurityPolicy; import org.terasology.gestalt.module.sandbox.StandardPermissionProviderFactory; +import org.terasology.gestalt.naming.Name; +import org.terasology.gestalt.naming.Version; +import org.terasology.nui.UIWidget; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.ReflectPermission; import java.nio.file.Paths; import java.security.Policy; +import java.util.Collections; import java.util.Set; /** @@ -177,6 +184,14 @@ public void init() throws Exception { try { ModuleFactory moduleFactory = new ModuleFactory(); engineModule = moduleFactory.createPackageModule("org.destinationsol"); + // In order for the NUI widgets to be detected, they first need to be found and cached. The build script + // reflects over the NUI jar and saves a list of all the widgets within the engine's reflections.cache. + // TODO: Find a better way to do this. + Module nuiModule = new Module(new ModuleMetadata(new Name("nui"), new Version("2.0.0")), new EmptyFileSource(), + Collections.emptyList(), new Reflections("org.terasology.nui"), x -> { + String classPackageName = Reflection.getPackageName(x); + return "org.terasology.nui".equals(classPackageName) || classPackageName.startsWith("org.terasology.nui."); + }); // scan for all standard modules registry = new TableModuleRegistry(); @@ -186,6 +201,7 @@ public void init() throws Exception { Set requiredModules = Sets.newHashSet(); registry.add(engineModule); + registry.add(nuiModule); requiredModules.addAll(registry); loadEnvironment(requiredModules); diff --git a/engine/src/main/java/org/destinationsol/ui/SolInputManager.java b/engine/src/main/java/org/destinationsol/ui/SolInputManager.java index 8ebe33414..fd61f2701 100644 --- a/engine/src/main/java/org/destinationsol/ui/SolInputManager.java +++ b/engine/src/main/java/org/destinationsol/ui/SolInputManager.java @@ -353,7 +353,9 @@ public void draw(UiDrawer uiDrawer, SolApplication solApplication) { if (tutorialManager != null && getTopScreen() != game.getScreens().menuScreen) { tutorialManager.draw(uiDrawer); } + } + public void drawCursor(UiDrawer uiDrawer) { if (currCursor != null) { uiDrawer.draw(currCursor, CURSOR_SZ, CURSOR_SZ, CURSOR_SZ / 2, CURSOR_SZ / 2, mousePos.x, mousePos.y, 0, SolColor.WHITE); } diff --git a/engine/src/main/java/org/destinationsol/ui/nui/NUIManager.java b/engine/src/main/java/org/destinationsol/ui/nui/NUIManager.java new file mode 100644 index 000000000..808858428 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/ui/nui/NUIManager.java @@ -0,0 +1,321 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.ui.nui; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import org.destinationsol.CommonDrawer; +import org.destinationsol.GameOptions; +import org.destinationsol.SolApplication; +import org.destinationsol.assets.Assets; +import org.destinationsol.assets.sound.OggSound; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.input.InputType; +import org.terasology.input.MouseInput; +import org.terasology.input.device.KeyboardAction; +import org.terasology.input.device.KeyboardDevice; +import org.terasology.input.device.MouseAction; +import org.terasology.input.device.MouseDevice; +import org.terasology.nui.FocusManager; +import org.terasology.nui.FocusManagerImpl; +import org.terasology.nui.TabbingManager; +import org.terasology.nui.UITextureRegion; +import org.terasology.nui.backends.libgdx.LibGDXCanvasRenderer; +import org.terasology.nui.backends.libgdx.LibGDXKeyboardDevice; +import org.terasology.nui.backends.libgdx.LibGDXMouseDevice; +import org.terasology.nui.backends.libgdx.NUIInputProcessor; +import org.terasology.nui.canvas.CanvasImpl; +import org.terasology.nui.events.NUIKeyEvent; +import org.terasology.nui.events.NUIMouseButtonEvent; +import org.terasology.nui.events.NUIMouseWheelEvent; +import org.terasology.nui.skin.UISkin; +import org.terasology.nui.widgets.UIButton; +import org.terasology.nui.widgets.UIText; + +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; + +/** + * The NUI Manager is responsible for the initialisation and interaction between the NUI library and the game. + * It manages the rendering and update cycles of NUI widgets, which are contained in {@link NUIScreenLayer} + * containers. Each NUIScreenLayer manages its own UI-specific logic and rendering. + */ +public class NUIManager { + /** + * This LibGDX renderer used for NUI. It shares a SpriteBatch with the main game, although not a ShapeRenderer. + */ + private LibGDXCanvasRenderer canvasRenderer; + /** + * The game's canvas, which is used for all NUI rendering operations. See also {@link NUIManager#canvasRenderer}. + */ + private CanvasImpl canvas; + /** + * A blank white texture, used by-default for the text cursor. + */ + private UITextureRegion whiteTexture; + /** + * The NUI mouse device. Receives input directly form LibGDX, independently of the game. + */ + private MouseDevice mouse; + /** + * The NUI keyboard device. Receives input directly from LibHDX, independently of the game. + */ + private KeyboardDevice keyboard; + /** + * This allows NUI to determine which widgets are in-focus at the moment, which is used primarily for tabbing. + */ + private FocusManager focusManager; + /** + * The default UI skin used by all widgets. + */ + private UISkin skin; + + /** + * The UI stack. The elements are rendered from most recently added to least recent, so a stack-like structure + * was used. + */ + private Deque uiScreens = new LinkedList<>(); + + private static final String WHITE_TEXTURE_URN = "engine:uiWhiteTex"; + private static final String DEFAULT_SKIN_URN = "engine:default"; + private static final String BUTTON_CLICK_URN = "engine:uiHover"; + /** + * The value 0.9 was found from {@link org.destinationsol.ui.SolInputManager#playClick}, so it was copied here to + * retain the same click sound as the built-in UI. + */ + private static final float BUTTON_CLICK_PITCH = 0.9f; + + /** + * Creates and initialises a new NUIManager instance, which involves initialising a canvas and NUI input handlers. + * @param solApplication the application to use for initialisation + * @param commonDrawer used to directly access the game's LibGDX {@link com.badlogic.gdx.graphics.g2d.SpriteBatch} + * @param options used to initialise the UI scale with its previously-saved value + */ + public NUIManager(SolApplication solApplication, CommonDrawer commonDrawer, GameOptions options) { + NUIInputProcessor.CONSUME_INPUT = true; + + mouse = new LibGDXMouseDevice(); + keyboard = new LibGDXKeyboardDevice(); + canvasRenderer = new LibGDXCanvasRenderer(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), + commonDrawer.getSpriteBatch(), new ShapeRenderer(), false, true); + focusManager = new FocusManagerImpl(); + whiteTexture = Assets.getDSTexture(WHITE_TEXTURE_URN).getUiTexture(); + skin = Assets.getAssetHelper().get(new ResourceUrn(DEFAULT_SKIN_URN), UISkin.class).get(); + + canvas = new CanvasImpl(canvasRenderer, focusManager, keyboard, mouse, whiteTexture, skin, 100); + TabbingManager.setFocusManager(focusManager); + + OggSound sound = Assets.getSound(BUTTON_CLICK_URN); + sound.setBasePitch(BUTTON_CLICK_PITCH); + + // NUI widgets do not know how to obtain assets directly, so we need to provide defaults values here. + UIButton.DEFAULT_CLICK_SOUND = sound; + UIText.DEFAULT_CURSOR_TEXTURE = whiteTexture; + + // NOTE: SolApplication::addResizeSubscriber is not intended to be static, so use the instance form for compatibility + solApplication.addResizeSubscriber(() -> resize(Gdx.graphics.getBackBufferWidth(), Gdx.graphics.getBackBufferHeight())); + + setUiScale(options.getNuiUiScale()); + } + + /** + * Processes NUI input events (Keyboard and Mouse) and updates all UI layers. + * @param solApplication the application to use + */ + public void update(SolApplication solApplication) { + canvas.processMousePosition(mouse.getMousePosition()); + canvas.setGameTime(System.currentTimeMillis()); + + for (KeyboardAction action : keyboard.getInputQueue()) { + NUIKeyEvent event = new NUIKeyEvent(mouse, keyboard, action.getInput(), action.getInputChar(), action.getState()); + + if (focusManager.getFocus() != null) { + focusManager.getFocus().onKeyEvent(event); + } + + for (NUIScreenLayer uiScreen : uiScreens) { + if (uiScreen.onKeyEvent(event) || uiScreen.isBlockingInput() || event.isConsumed()) { + break; + } + } + } + + for (MouseAction action : mouse.getInputQueue()) { + if (action.getInput().getType() == InputType.MOUSE_BUTTON) { + if (action.getState().isDown()) { + canvas.processMouseClick((MouseInput) action.getInput(), action.getMousePosition()); + } else { + canvas.processMouseRelease((MouseInput) action.getInput(), action.getMousePosition()); + } + + NUIMouseButtonEvent event = new NUIMouseButtonEvent((MouseInput) action.getInput(), action.getState(), action.getMousePosition()); + + if (focusManager.getFocus() != null) { + focusManager.getFocus().onMouseButtonEvent(event); + } + + for (NUIScreenLayer uiScreen : uiScreens) { + uiScreen.onMouseButtonEvent(event); + if (event.isConsumed()) { + break; + } + } + } else if (action.getInput().getType() == InputType.MOUSE_WHEEL) { + canvas.processMouseWheel(action.getTurns(), action.getMousePosition()); + + NUIMouseWheelEvent event = new NUIMouseWheelEvent(mouse, keyboard, action.getMousePosition(), action.getTurns()); + + if (focusManager.getFocus() != null) { + focusManager.getFocus().onMouseWheelEvent(event); + } + + for (NUIScreenLayer uiScreen : uiScreens) { + uiScreen.onMouseWheelEvent(event); + if (event.isConsumed()) { + break; + } + } + } + } + + for (NUIScreenLayer uiScreen : uiScreens) { + uiScreen.update(Gdx.graphics.getDeltaTime()); + } + } + + /** + * Renders all UI layers. + * @param gameDrawer used to directly access the game's LibGDX {@link com.badlogic.gdx.graphics.g2d.SpriteBatch} + */ + public void draw(CommonDrawer gameDrawer) { + gameDrawer.getSpriteBatch().flush(); + + canvas.preRender(); + + // NOTE: Need to render in the inverse to how they are updated, so the top screen is drawn last + Iterator screenIterator = uiScreens.descendingIterator(); + while (screenIterator.hasNext()) { + NUIScreenLayer screenLayer = screenIterator.next(); + canvas.setSkin(screenLayer.getSkin()); + canvas.drawWidget(screenLayer); + } + + canvas.postRender(); + + gameDrawer.getSpriteBatch().flush(); + } + + /** + * Obtains the topmost screen (rendered on-top of all others) + * @return the topmost screen + */ + public NUIScreenLayer getTopScreen() { + return uiScreens.peek(); + } + + /** + * Pushes a screen onto the UI stack. + * @param layer the screen to add + */ + public void pushScreen(NUIScreenLayer layer) { + uiScreens.push(layer); + + layer.setFocusManager(focusManager); + layer.setNuiManager(this); + layer.initialise(); + } + + /** + * Removes the topmost screen from the UI stack and returns it. + * @return the topmost screen + */ + public NUIScreenLayer popScreen() { + if (!uiScreens.isEmpty()) { + uiScreens.peek().onRemoved(); + } + return uiScreens.pop(); + } + + /** + * Removes a screen form the UI stack. It is no longer updated or drawn. + * @param screen the screen to remove + */ + public void removeScreen(NUIScreenLayer screen) { + screen.onRemoved(); + uiScreens.remove(screen); + } + + /** + * States if a screen is currently present on the UI stack. + * @param screen the screen to search for + * @return true if the screen is currently on the UI stack, otherwise false + */ + public boolean hasScreen(NUIScreenLayer screen) { + return uiScreens.contains(screen); + } + + /** + * States if a screen of a specified type is present on the UI stack. + * @param type the type of screen to search for + * @return true if a screen of the specified type is currently on the UI stack, otherwise false + */ + public boolean hasScreenOfType(Class type) { + for (NUIScreenLayer screenLayer : uiScreens) { + if (screenLayer.getClass().isAssignableFrom(type)) { + return true; + } + } + + return false; + } + + /** + * Returns all of the UI screens currently on the UI stack. + * @return the UI stack + */ + public Deque getScreens() { + return uiScreens; + } + + /** + * Returns the default {@link UISkin} for widgets. + * @return the default {@link UISkin} + */ + public UISkin getDefaultSkin() { + return skin; + } + + /** + * Re-sizes the current canvas to a particular width and height. This is not always the same as the game's current + * rendering resolution. + * @param width the width to use + * @param height the height to use + */ + public void resize(int width, int height) { + canvasRenderer.resize(width, height); + } + + /** + * Sets the UI scale to a specified value. This will internally render the UI at specified scale and up-scale it + * to the target resolution. + * @param scale the new UI scale to use + */ + public void setUiScale(float scale) { + canvas.setUiScale(scale); + canvasRenderer.setUiScale(1.0f / scale); + } +} diff --git a/engine/src/main/java/org/destinationsol/ui/nui/NUIScreenLayer.java b/engine/src/main/java/org/destinationsol/ui/nui/NUIScreenLayer.java new file mode 100644 index 000000000..8657cadcc --- /dev/null +++ b/engine/src/main/java/org/destinationsol/ui/nui/NUIScreenLayer.java @@ -0,0 +1,193 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.ui.nui; + +import org.joml.Vector2i; +import org.terasology.input.ButtonState; +import org.terasology.input.Keyboard; +import org.terasology.nui.AbstractWidget; +import org.terasology.nui.Canvas; +import org.terasology.nui.FocusManager; +import org.terasology.nui.LayoutConfig; +import org.terasology.nui.UIWidget; +import org.terasology.nui.events.NUIKeyEvent; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; + +/** + * A specialised {@link UIWidget} designed to contain the widgets comprising a single UI screen (or screen layer). + */ +public abstract class NUIScreenLayer extends AbstractWidget { + /** + * The contents of the UI screen. It contains all of the widgets in the screen and can be queried with the + * {@link #find} method. + */ + @LayoutConfig + protected UIWidget contents; + /** + * The focus manager assigned to the UI screen. + */ + protected FocusManager focusManager; + /** + * The game's NUI Manager. + */ + protected NUIManager nuiManager; + + /** + * This should just render the widgets contained in the UI screen, however it can be overridden to render custom + * graphics as well. + * @param canvas the canvas to draw on + */ + @Override + public void onDraw(Canvas canvas) { + canvas.drawWidget(contents); + } + + /** + * This updates the widgets contained in the UI screen. It is called every cycle and can therefore be used for + * updating the contents of the widgets as well. + * @param delta the time elapsed since the last update cycle + */ + @Override + public void update(float delta) { + super.update(delta); + contents.update(delta); + } + + /** + * UI Screens can not by-default become focused themselves, as the focus should be on their contents instead. + * @return false, by default + */ + @Override + public boolean canBeFocus() { + return false; + } + + /** + * Returns the preferred content size of this widget. + * + * @param canvas A {@link Canvas} on which this widget is drawn. + * @param sizeHint A {@link Vector2i} representing how much available space is for this widget. + * @return A {@link Vector2i} which represents the preferred size of this widget. + */ + @Override + public Vector2i getPreferredContentSize(Canvas canvas, Vector2i sizeHint) { + return sizeHint; + } + + /** + * UI Screens can potentially occupy any size, as their contents is not restricted. + * @param canvas the canvas to render to + * @return the maximum possible size (Integer.MAX_VALUE, Integer.MAX_VALUE). + */ + @Override + public Vector2i getMaxContentSize(Canvas canvas) { + return new Vector2i(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + /** + * Returns an iterator over elements of type {@code T}. + * + * @return an Iterator. + */ + @Override + public Iterator iterator() { + if (contents == null) { + return Collections.emptyIterator(); + } + return Arrays.asList(contents).iterator(); + } + + /** + * This manages by-default the escape key closing the UI screen, however it can be overridden for more specific + * behaviour, such (for example) as pressing a certain key to trigger a sub-menu. + * @param event the key event generated + * @return true, if the event should be consumed, otherwise false + */ + @Override + public boolean onKeyEvent(NUIKeyEvent event) { + if (escapeCloses() && event.getState() == ButtonState.UP && event.getKey() == Keyboard.Key.ESCAPE) { + nuiManager.removeScreen(this); + return true; + } + + return super.onKeyEvent(event); + } + + /** + * Called to initialise the UI screen, allowing it to register widget callbacks and assign widget values + * programmatically. This is called whenever the UI screen becomes visible. + */ + public void initialise() { + } + + /** + * Called when the UI screen is removed from display. + */ + public void onRemoved() { + } + + /** + * States if the inputs received by this UI screen should not be received by other UI screens underneath. + * @return true if inputs should be blocked, otherwise false + */ + public boolean isBlockingInput() { + return false; + } + + /** + * Is this UI screen closed when the escape key is pressed? + * @return true, if the screen should be closed + */ + protected boolean escapeCloses() { + return true; + } + + /** + * Sets the focus manager to use. + * Primary usage is in {@link NUIManager#pushScreen} + * @param focusManager the focus manager to use. + */ + void setFocusManager(FocusManager focusManager) { + this.focusManager = focusManager; + } + + /** + * Returns the focus manager currently assigned to this UI screen. + * @return the currently assigned focus manager + */ + protected FocusManager getFocusManager() { + return focusManager; + } + + /** + * Sets the game's NUI Manager. + * @param nuiManager the game's NUI Manager + */ + void setNuiManager(NUIManager nuiManager) { + this.nuiManager = nuiManager; + } + + /** + * Returns the game's NUI Manager. + * @return the game's NUI Manager + */ + protected NUIManager getNuiManager() { + return nuiManager; + } +} diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/ConsoleScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/ConsoleScreen.java new file mode 100644 index 000000000..8d3ccce37 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/ConsoleScreen.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.ui.nui.screens; + +import org.destinationsol.game.console.Console; +import org.destinationsol.game.console.ConsoleImpl; +import org.destinationsol.game.console.CyclingTabCompletionEngine; +import org.destinationsol.game.console.Message; +import org.destinationsol.ui.nui.NUIScreenLayer; +import org.destinationsol.ui.nui.widgets.UICommandEntry; +import org.terasology.input.MouseInput; +import org.terasology.nui.BaseInteractionListener; +import org.terasology.nui.Canvas; +import org.terasology.nui.FontColor; +import org.terasology.nui.InteractionListener; +import org.terasology.nui.databinding.ReadOnlyBinding; +import org.terasology.nui.events.NUIMouseClickEvent; +import org.terasology.nui.layouts.ScrollableArea; +import org.terasology.nui.widgets.UIText; + +import java.util.List; + +/** + * The console screen. You can enter commands into this UI and they will be executed. The logic is in {@link Console}. + * This was taken from Terasology originally. + */ +public class ConsoleScreen extends NUIScreenLayer { + private Console console; + private UICommandEntry commandLine; + private boolean welcomePrinted; + private boolean screenClosed; + + private InteractionListener screenListener = new BaseInteractionListener() { + @Override + public boolean onMouseClick(NUIMouseClickEvent event) { + if (event.getMouseButton() == MouseInput.MOUSE_LEFT && commandLine != null) { + focusManager.setFocus(commandLine); + } + return true; + } + }; + + @Override + public void initialise() { + screenClosed = false; + + if (!welcomePrinted) { + console = ConsoleImpl.instance; + + final ScrollableArea scrollArea = find("scrollArea", ScrollableArea.class); + scrollArea.moveToBottom(); + + commandLine = find("commandLine", UICommandEntry.class); + commandLine.setTabCompletionEngine(new CyclingTabCompletionEngine(console)); + commandLine.bindCommandHistory(new ReadOnlyBinding>() { + @Override + public List get() { + return console.getPreviousCommands(); + } + }); + commandLine.subscribe(widget -> { + String text = commandLine.getText(); + if (!text.isEmpty()) { + console.execute(text); + } + scrollArea.moveToBottom(); + }); + + final UIText history = find("messageHistory", UIText.class); + history.bindText(new ReadOnlyBinding() { + @Override + public String get() { + StringBuilder messageList = new StringBuilder(); + for (Message message : console.getMessages()) { + messageList.append(FontColor.getColored(message.getMessage(), new org.terasology.nui.Color(message.getType().getColor().toIntBits()))); + messageList.append(Console.NEW_LINE); + } + return messageList.toString(); + } + }); + + onOpened(); + } + + focusManager.setFocus(commandLine); + } + + public void onOpened() { + if (!welcomePrinted) { + console.addMessage("Welcome to the world of Destination Sol! Your journey begins!" + Console.NEW_LINE + + "Type 'help' to see a list with available commands or 'help ' for command details." + Console.NEW_LINE + + "Text parameters do not need quotes, unless containing spaces. No commas between parameters." + Console.NEW_LINE + + "You can use auto-completion by typing a partial command then hitting [tab] - examples:" + Console.NEW_LINE + Console.NEW_LINE + + "go + [tab] => 'godMode'" + Console.NEW_LINE + + "help gh + [tab] => 'help godMode' (can auto complete commands fed to help)" + Console.NEW_LINE + + "(use [tab] again to cycle between choices)" + Console.NEW_LINE + + "gM + [tab] => 'godMode' (camel casing abbreviated commands)" + Console.NEW_LINE); + welcomePrinted = true; + } + } + + @Override + public void onRemoved() { + screenClosed = true; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.addInteractionRegion(screenListener, canvas.getRegion()); + } + + @Override + public boolean isBlockingInput() { + return true; + } + + public boolean isConsoleJustClosed() { + if (screenClosed) { + screenClosed = false; + return true; + } + + return false; + } +} diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/MigTestScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/MigTestScreen.java new file mode 100644 index 000000000..4b0d5dbe4 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/MigTestScreen.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.ui.nui.screens; + +import org.destinationsol.ui.nui.NUIScreenLayer; +import org.terasology.nui.widgets.UIDropdown; +import org.terasology.nui.widgets.UIDropdownScrollable; +import org.terasology.nui.widgets.UIList; + +import java.util.Arrays; +import java.util.List; + +/** + * A test screen used to verify that NUI widgets render and interact correctly. It was taken from Terasology. + */ +public class MigTestScreen extends NUIScreenLayer { + @Override + public void initialise() { + List values = Arrays.asList("one", "two", "three", "12345678901234567890"); + String selectedValue = values.get(1); + + for (String id : new String[]{"dropdown1", "dropdown2", "dropdown3", "dropdown4"}) { + contents.find(id, UIDropdown.class).setOptions(values); + contents.find(id, UIDropdown.class).setSelection(selectedValue); + } + + for (String id : new String[]{"dropdownScrollable1", "dropdownScrollable2", "dropdownScrollable3", "dropdownScrollable4"}) { + contents.find(id, UIDropdownScrollable.class).setVisibleOptions(2); + contents.find(id, UIDropdownScrollable.class).setOptions(values); + contents.find(id, UIDropdownScrollable.class).setSelection(selectedValue); + } + + for (String id : new String[]{"list1", "list2", "list3", "list4"}) { + contents.find(id, UIList.class).setList(values); + } + } +} diff --git a/engine/src/main/java/org/destinationsol/ui/nui/widgets/UICommandEntry.java b/engine/src/main/java/org/destinationsol/ui/nui/widgets/UICommandEntry.java new file mode 100644 index 000000000..62837d240 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/ui/nui/widgets/UICommandEntry.java @@ -0,0 +1,115 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.ui.nui.widgets; + +import com.google.common.collect.Lists; +import org.destinationsol.game.console.TabCompletionEngine; +import org.terasology.input.Keyboard; +import org.terasology.nui.databinding.Binding; +import org.terasology.nui.databinding.DefaultBinding; +import org.terasology.nui.events.NUIKeyEvent; +import org.terasology.nui.widgets.UIText; + +import java.util.List; + +/** + * A UI Widget used for command-line input and tab-completion in the console screen. + */ +public class UICommandEntry extends UIText { + private Binding> commandHistory = new DefaultBinding<>(Lists.newArrayList()); + private int index; + private TabCompletionEngine tabCompletionEngine; + + public UICommandEntry() { + subscribe((int oldPosition, int newPosition) -> { + if (tabCompletionEngine == null) { + return; + } + + tabCompletionEngine.reset(); + }); + } + + @Override + public boolean onKeyEvent(NUIKeyEvent event) { + if (event.isDown()) { + int id = event.getKey().getId(); + + if (id != Keyboard.KeyId.TAB && tabCompletionEngine != null) { + tabCompletionEngine.reset(); + } + + switch (id) { + case Keyboard.KeyId.UP: + if (index > 0) { + index--; + if (getCommandHistory().size() > index) { + setText(getCommandHistory().get(index)); + } + setCursorPosition(getText().length()); + } + return true; + case Keyboard.KeyId.DOWN: + if (index < getCommandHistory().size()) { + index++; + if (index == getCommandHistory().size()) { + setText(""); + } else { + setText(getCommandHistory().get(index)); + setCursorPosition(getText().length()); + } + } + return true; + case Keyboard.KeyId.TAB: + if (tabCompletionEngine != null) { + setText(tabCompletionEngine.complete(getText())); + setCursorPosition(getText().length(), true, false); + return true; + } + break; + case Keyboard.KeyId.ENTER: + boolean consumed = super.onKeyEvent(event); + setText(""); + index = getCommandHistory().size(); + return consumed; + default: + return super.onKeyEvent(event); + } + } + return false; + } + + public void bindCommandHistory(Binding> binding) { + commandHistory = binding; + index = commandHistory.get().size(); + } + + public List getCommandHistory() { + return commandHistory.get(); + } + + public void setCommandHistory(List val) { + commandHistory.set(val); + } + + public TabCompletionEngine getTabCompletionEngine() { + return tabCompletionEngine; + } + + public void setTabCompletionEngine(TabCompletionEngine tabCompletionEngine) { + this.tabCompletionEngine = tabCompletionEngine; + } +} diff --git a/engine/src/main/resources/org/destinationsol/assets/skins/console.skin b/engine/src/main/resources/org/destinationsol/assets/skins/console.skin new file mode 100644 index 000000000..b96a3dd37 --- /dev/null +++ b/engine/src/main/resources/org/destinationsol/assets/skins/console.skin @@ -0,0 +1,54 @@ +{ + "inherit": "engine:default", + "elements": { + "UILabel": { + "text-align-horizontal": "LEFT", + "text-align-vertical": "top", + "text-shadowed": false + }, + "ScrollableArea": { + "background": "engine:buttonDown", + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + } + } + }, + "families": { + "history": { + "elements": { + "UIText": { + "background": "", + "text-shadowed": false, + "background-border": { + "top": 0, + "bottom": 0, + "left": 0, + "right": 0 + }, + "margin": { + "top": 0, + "bottom": 0, + "left": 0, + "right": 0 + } + } + } + }, + "commandInput": { + "elements": { + "UICommandEntry": { + "background": "engine:boxLight" + } + } + } + } +} diff --git a/engine/src/main/resources/org/destinationsol/assets/skins/default.skin b/engine/src/main/resources/org/destinationsol/assets/skins/default.skin new file mode 100644 index 000000000..189d3cea0 --- /dev/null +++ b/engine/src/main/resources/org/destinationsol/assets/skins/default.skin @@ -0,0 +1,588 @@ +{ + "text-shadowed": true, + "font": "engine:main", + "elements": { + "UILabel": { + "text-align-vertical": "top", + "modes": { + "enabled": { + "text-color": "FFFFFFFF" + }, + "disabled": { + "text-color": "888888FF" + } + } + }, + "UITooltip": { + "background": "engine:area", + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "align-horizontal": "left", + "align-vertical": "bottom", + "text-align-horizontal": "left" + }, + "UIText": { + "text-align-horizontal": "left", + "text-align-vertical": "top", + "hint-text-color": "C0C0C0FF", + "background": "engine:box", + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "modes": { + "disabled": { + "text-color": "888888FF", + "background": "engine:boxDisabled" + } + } + }, + "ResettableUIText": { + "background": "engine:resetBox", + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 29 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 31 + }, + "modes": { + "disabled": { + "text-color": "888888FF", + "background": "engine:resetBoxDisabled" + } + } + }, + "UIImage": { + "texture-scale-mode": "scale fit" + }, + "UIImageSlideshow": { + "texture-scale-mode": "scale fit" + }, + "RowLayout": { + }, + "UIBox": { + "background": "engine:area", + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "margin": { + "top": 12, + "bottom": 12, + "left": 12, + "right": 12 + } + }, + "UITabBox": { + "background": "engine:area", + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "margin": { + "top": 12, + "bottom": 12, + "left": 12, + "right": 12 + } + }, + "UILoadBar": { + "background": "engine:area", + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "min-width": 256, + "max-width": 512, + "fixed-height": 20 + }, + "UIList": { + "parts": { + "item": { + "text-align-horizontal": "center", + "text-align-vertical": "middle", + "background": "button", + "background-border": { + "top": 2, + "bottom": 2, + "left": 2, + "right": 2 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 2, + "right": 2 + }, + "texture-scale-mode": "scale fit", + "modes": { + "hover": { + "background": "buttonOver" + }, + "active": { + "background": "buttonDown", + "text-color": "FFFF00FF" + }, + "disabled": { + "background": "button", + "text-color": "BBBBBBFF" + } + } + } + } + }, + "UIDropdown": { + "text-align-horizontal": "left", + "fixed-height": 32, + "parts": { + "base": { + "background": "dropdown", + "background-border": { + "top": 3, + "bottom": 3, + "left": 3, + "right": 29 + }, + "margin": { + "top": 3, + "bottom": 3, + "left": 5, + "right": 31 + }, + "modes": { + "active": { + "background": "dropdownActive" + }, + "disabled": { + "text-color": "888888FF", + "background": "dropdownDisabled" + } + } + }, + "list": { + "background": "dropdownList", + "background-border": { + "top": 0, + "bottom": 3, + "left": 3, + "right": 3 + }, + "margin": { + "top": 3, + "bottom": 3, + "left": 5, + "right": 5 + } + }, + "list-item": { + "margin": { + "top": 3, + "bottom": 3, + "left": 5, + "right": 5 + }, + "modes": { + "hover": { + "text-color": "FFFF00FF", + "background": "dropdownListItemActive" + } + } + } + } + }, + "UIDropdownScrollable": { + "text-align-horizontal": "left", + "fixed-height": 32, + "parts": { + "base": { + "background": "dropdown", + "background-border": { + "top": 3, + "bottom": 3, + "left": 3, + "right": 29 + }, + "margin": { + "top": 3, + "bottom": 3, + "left": 5, + "right": 31 + }, + "modes": { + "active": { + "background": "dropdownActive" + }, + "disabled": { + "text-color": "888888FF", + "background": "dropdownDisabled" + } + } + }, + "list": { + "background": "dropdownList", + "background-border": { + "top": 0, + "bottom": 3, + "left": 3, + "right": 3 + }, + "margin": { + "top": 3, + "bottom": 3, + "left": 5, + "right": 5 + } + }, + "list-item": { + "margin": { + "top": 3, + "bottom": 3, + "left": 5, + "right": 5 + }, + "modes": { + "hover": { + "text-color": "FFFF00FF", + "background": "dropdownListItemActive" + } + } + } + } + }, + "UIButton": { + "text-align-horizontal": "center", + "text-align-vertical": "middle", + "background": "button", + "background-border": { + "top": 2, + "bottom": 2, + "left": 2, + "right": 2 + }, + "margin": { + "top": 2, + "bottom": 2, + "left": 10, + "right": 10 + }, + "texture-scale-mode": "scale fit", + "modes": { + "hover": { + "background": "buttonOver" + }, + "down": { + "background": "buttonDown", + "text-color": "FFFF00FF" + }, + "disabled": { + "background": "button", + "text-color": "BBBBBBFF" + } + } + }, + "UIInputBind": { + "text-align-horizontal": "center", + "text-align-vertical": "middle", + "background": "button", + "background-border": { + "top": 2, + "bottom": 2, + "left": 2, + "right": 2 + }, + "texture-scale-mode": "scale fit", + "modes": { + "hover": { + "background": "buttonOver" + }, + "active": { + "background": "buttonDown", + "text-color": "FFFF00FF" + } + } + }, + "UICheckbox": { + "background": "engine:checkbox", + "fixed-width": 24, + "fixed-height": 24, + "align-horizontal": "left", + "modes": { + "hover": { + "background": "engine:checkboxHover" + }, + "active": { + "background": "engine:checkboxChecked" + }, + "hover-active": { + "background": "engine:checkboxCheckedHover" + }, + "disabled": { + "background": "engine:checkboxDisabled" + }, + "disabled-active": { + "background": "engine:checkboxCheckedDisabled" + } + } + }, + "ScrollableArea": { + "background": "engine:box", + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + } + }, + "ColumnLayout": { + "align-vertical": "TOP" + }, + "UIScrollbar": { + "parts": { + "sliderVertical": { + "background": "engine:sliderTrack", + "fixed-width": 8, + "background-border": { + "top": 3, + "bottom": 3, + "left": 0, + "right": 0 + } + }, + "sliderHorizontal": { + "background": "engine:sliderTrack", + "fixed-height": 8, + "background-border": { + "top": 0, + "bottom": 0, + "left": 3, + "right": 3 + } + }, + "handle": { + "background": "engine:handle", + "fixed-width": 16, + "fixed-height": 16 + } + } + }, + "UISlider": { + "text-align-horizontal": "center", + "text-align-vertical": "middle", + "fixed-height": 32, + "parts": { + "slider": { + "background": "engine:sliderTrack", + "fixed-height": 8, + "min-width": 8, + "background-border": { + "top": 0, + "bottom": 0, + "left": 3, + "right": 3 + } + }, + "ticker": { + "background": "engine:box", + "fixed-height": 32, + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 5, + "right": 5 + }, + "modes": { + "active": { + "background": "engine:boxActive" + }, + "disabled": { + "text-color": "888888FF", + "background": "engine:boxDisabled" + } + } + } + } + }, + "UIDoubleSlider": { + "text-align-horizontal": "center", + "text-align-vertical": "middle", + "fixed-height": 32, + "parts": { + "slider": { + "background": "engine:sliderTrack", + "fixed-height": 8, + "min-width": 8, + "background-border": { + "top": 0, + "bottom": 0, + "left": 3, + "right": 3 + } + }, + "tickerLeft": { + "background": "engine:box", + "fixed-height": 32, + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 5, + "right": 5 + }, + "modes": { + "active": { + "background": "engine:boxActive" + }, + "disabled": { + "text-color": "888888FF", + "background": "engine:boxDisabled" + } + } + }, + "tickerRight": { + "background": "engine:box", + "fixed-height": 32, + "background-border": { + "top": 4, + "bottom": 4, + "left": 4, + "right": 4 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 5, + "right": 5 + }, + "modes": { + "active": { + "background": "engine:boxActive" + }, + "enabled": { + "text-color": "FFFFFFFF" + }, + "disabled": { + "text-color": "888888FF", + "background": "engine:boxDisabled" + } + } + } + } + }, + "UITreeView": { + "parts": { + "expand-button": { + "background": "button", + "background-border": { + "top": 2, + "bottom": 2, + "left": 2, + "right": 2 + }, + "texture-scale-mode": "scale fit", + "modes": { + "contract": { + "background": "contract" + }, + "contract-hover": { + "background": "contractOver" + }, + "expand": { + "background": "expand" + }, + "expand-hover": { + "background": "expandOver" + } + } + }, + "tree-node": { + "text-align-horizontal": "left", + "text-align-vertical": "middle", + "background": "button", + "background-border": { + "top": 2, + "bottom": 2, + "left": 2, + "right": 2 + }, + "margin": { + "top": 4, + "bottom": 4, + "left": 2, + "right": 2 + }, + "texture-scale-mode": "scale fit", + "modes": { + "hover": { + "background": "buttonOver" + }, + "active": { + "background": "buttonDown", + "text-color": "FFFF00FF" + }, + "disabled": { + "background": "button", + "text-color": "888888FF" + }, + "hover-disabled": { + "background": "buttonOver", + "text-color": "888888FF" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/engine/src/main/resources/org/destinationsol/assets/skins/migTestSkin.skin b/engine/src/main/resources/org/destinationsol/assets/skins/migTestSkin.skin new file mode 100644 index 000000000..a74fc705e --- /dev/null +++ b/engine/src/main/resources/org/destinationsol/assets/skins/migTestSkin.skin @@ -0,0 +1,129 @@ +{ + "inherit": "engine:default", + "elements": { + "UIButton": { + "modes": { + "disabled": { + "text-color": "AAAAAAFF" + } + } + }, + "UIButtonWebBrowser": { + "text-color": "8B85FFFF" + } + }, + "families": { + "menu-options": { + "elements": { + "UIButton": { + "min-width": 192, + "min-height": 32 + } + } + }, + "option-grid": { + "elements": { + "UILabel": { + "text-align-vertical": "middle", + "text-align-horizontal": "right" + } + } + }, + "title": { + "font": "main", + "text-color": "F0F0F0FF" + }, + "subtitle": { + "font": "main", + "text-color": "F0F0F0FF" + }, + "error": { + "text-color": "FF4444FF" + }, + "warning": { + "text-color": "FF4500FF" + }, + "highlight": { + "text-color": "FFFF00FF", + "font": "main" + }, + "subheading": { + "font": "main", + "text-align-horizontal": "left", + "margin": { + "bottom": 4, + "left": 4 + } + }, + "left-label": { + "elements": { + "UILabel": { + "text-align-vertical": "bottom", + "text-align-horizontal": "left" + } + } + }, + "module-list": { + "elements": { + "UIList": { + "parts": { + "item": { + "modes": { + "gameplay": { + "text-color": "8B85FFFF" + }, + "enabled": { + "text-color": "00FF00FF" + }, + "dependency": { + "text-color": "FFFF00FF" + }, + "available": { + "text-color": "FFFFFFFF" + }, + "disabled": { + "text-color": "AAAAAAFF" + }, + "invalid": { + "text-color": "FF0000FF" + }, + "latest": { + "text-color": "FFFF00FF" + }, + "strict": { + "text-color": "FFFFFFFF" + } + } + } + } + } + } + }, + "biome-list": { + "elements": { + "UIList": { + "parts": { + "item": { + "modes": { + "internal": { + "text-color": "FFFFFFFF" + }, + "external": { + "text-color": "8B85FFFF" + } + } + } + } + } + } + }, + "description": { + "elements": { + "UILabel": { + "text-align-vertical": "top", + "text-align-horizontal": "left" + } + } + } + } +} diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/area.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/area.png new file mode 100644 index 000000000..6809968f8 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/area.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/box.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/box.png new file mode 100644 index 000000000..83885716b Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/box.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/boxActive.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/boxActive.png new file mode 100644 index 000000000..7e7fc0e9b Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/boxActive.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/boxDisabled.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/boxDisabled.png new file mode 100644 index 000000000..44652a8a2 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/boxDisabled.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/boxLight.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/boxLight.png new file mode 100644 index 000000000..0bb39e2ce Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/boxLight.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/button.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/button.png new file mode 100644 index 000000000..38dd795ff Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/button.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/buttonDown.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/buttonDown.png new file mode 100644 index 000000000..5bd1bc55c Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/buttonDown.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/buttonOver.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/buttonOver.png new file mode 100644 index 000000000..a4824e7a7 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/buttonOver.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkbox.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkbox.png new file mode 100644 index 000000000..2df2117bd Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkbox.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxChecked.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxChecked.png new file mode 100644 index 000000000..da7a961f0 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxChecked.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxCheckedDisabled.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxCheckedDisabled.png new file mode 100644 index 000000000..eb28715ab Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxCheckedDisabled.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxCheckedHover.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxCheckedHover.png new file mode 100644 index 000000000..cd2a8f767 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxCheckedHover.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxDisabled.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxDisabled.png new file mode 100644 index 000000000..2df2117bd Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxDisabled.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxHover.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxHover.png new file mode 100644 index 000000000..cdb1bde46 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/checkboxHover.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdown.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdown.png new file mode 100644 index 000000000..4b2753cd9 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdown.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownActive.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownActive.png new file mode 100644 index 000000000..4d6ebfd23 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownActive.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownDisabled.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownDisabled.png new file mode 100644 index 000000000..79903b7b1 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownDisabled.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownList.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownList.png new file mode 100644 index 000000000..fab68f850 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownList.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownListItemActive.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownListItemActive.png new file mode 100644 index 000000000..dbdf82500 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/dropdownListItemActive.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/handle.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/handle.png new file mode 100644 index 000000000..58883209f Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/handle.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/resetBox.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/resetBox.png new file mode 100644 index 000000000..9beeb6a3b Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/resetBox.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/resetBoxDisabled.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/resetBoxDisabled.png new file mode 100644 index 000000000..2a549f28d Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/resetBoxDisabled.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/sliderTrack.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/sliderTrack.png new file mode 100644 index 000000000..54862a3a0 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/sliderTrack.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/LICENSE_NOTES b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/LICENSE_NOTES new file mode 100644 index 000000000..db8f8f6ff --- /dev/null +++ b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/LICENSE_NOTES @@ -0,0 +1,45 @@ +The icons in this folder are not under the same license like the source code. See /docs/Credits.md for more information. + +---Image info from Terasology's docs/Credits.md--- +Icons: +Some NUI editor icons belong to "Fugue Icons" by Yusuke Kamiyamane, licensed under the Creative Commons Attribution 3.0 License: + editor_array.png + editor_attribute.png + editor_object.png + editor_uibox.png + editor_uibutton.png + editor_uicheckbox.png + editor_uidoubleslider.png + editor_uidropdown.png + editor_uidropdownscrollable.png + editor_uiimage.png + editor_uilabel.png + editor_uilist.png + editor_uiloadbar.png + editor_uiscrollbar.png + editor_uislider.png + editor_uitext.png + editor_uitextentry.png + editor_uitooltip.png + editor_uitreeview.png + editor_zoomablelayout.png + +Some icons have been made by Florian Köberle and are licensed under the Creative Commons Attribution 4.0 License: + editor_cardLayout.png + editor_columnlayout.png + editor_flowlayout.png + editor_miglayout.png + editor_relativelayout.png + editor_rowlayout.png + editor_uispace.png + contract.png + contractOver.png + expand.png + expandOver.png + +Icons prepared by kartikey0303 (also CC BY 4.0) + checkboxChecked.png + checkboxCheckedDisabled.png + checkboxCheckedHover.png + +-- NOTE: Only the items in the "terasology" folder are covered by this license. Any other images may be covered by other licenses. \ No newline at end of file diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/radial/radialUnit.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/radial/radialUnit.png new file mode 100644 index 000000000..c27c56e59 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/radial/radialUnit.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/radial/radialUnitSelected.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/radial/radialUnitSelected.png new file mode 100644 index 000000000..870b6937f Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/radial/radialUnitSelected.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/statusBar.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/statusBar.png new file mode 100644 index 000000000..25283d257 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/statusBar.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorder.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorder.png new file mode 100644 index 000000000..af5673327 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorder.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorder.texinfo b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorder.texinfo new file mode 100644 index 000000000..15f1ede8c --- /dev/null +++ b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorder.texinfo @@ -0,0 +1,3 @@ +{ + "filterMode" : "Linear" +} diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorderOver.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorderOver.png new file mode 100644 index 000000000..24d6fac8f Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorderOver.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorderOver.texinfo b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorderOver.texinfo new file mode 100644 index 000000000..15f1ede8c --- /dev/null +++ b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/testWindowBorderOver.texinfo @@ -0,0 +1,3 @@ +{ + "filterMode" : "Linear" +} diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/LICENSE_NOTES b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/LICENSE_NOTES new file mode 100644 index 000000000..6fb9b6c1d --- /dev/null +++ b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/LICENSE_NOTES @@ -0,0 +1,43 @@ +The icons in this folder are not under the same license like the source code. See /docs/Credits.md for more information. + +---Image info from Terasology's docs/Credits.md--- +Icons: +Some NUI editor icons belong to "Fugue Icons" by Yusuke Kamiyamane, licensed under the Creative Commons Attribution 3.0 License: + editor_array.png + editor_attribute.png + editor_object.png + editor_uibox.png + editor_uibutton.png + editor_uicheckbox.png + editor_uidoubleslider.png + editor_uidropdown.png + editor_uidropdownscrollable.png + editor_uiimage.png + editor_uilabel.png + editor_uilist.png + editor_uiloadbar.png + editor_uiscrollbar.png + editor_uislider.png + editor_uitext.png + editor_uitextentry.png + editor_uitooltip.png + editor_uitreeview.png + editor_zoomablelayout.png + +Some icons have been made by Florian Köberle and are licensed under the Creative Commons Attribution 4.0 License: + editor_cardLayout.png + editor_columnlayout.png + editor_flowlayout.png + editor_miglayout.png + editor_relativelayout.png + editor_rowlayout.png + editor_uispace.png + contract.png + contractOver.png + expand.png + expandOver.png + +Icons prepared by kartikey0303 (also CC BY 4.0) + checkboxChecked.png + checkboxCheckedDisabled.png + checkboxCheckedHover.png \ No newline at end of file diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/contract.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/contract.png new file mode 100644 index 000000000..cce284105 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/contract.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/contractOver.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/contractOver.png new file mode 100644 index 000000000..3d6b2bab2 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/contractOver.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/expand.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/expand.png new file mode 100644 index 000000000..d45d643fa Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/expand.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/expandOver.png b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/expandOver.png new file mode 100644 index 000000000..38e6cc911 Binary files /dev/null and b/engine/src/main/resources/org/destinationsol/assets/textures/ui/nui/terasology/treeView/expandOver.png differ diff --git a/engine/src/main/resources/org/destinationsol/assets/ui/console.ui b/engine/src/main/resources/org/destinationsol/assets/ui/console.ui new file mode 100644 index 000000000..679968cb0 --- /dev/null +++ b/engine/src/main/resources/org/destinationsol/assets/ui/console.ui @@ -0,0 +1,52 @@ +{ + "type": "ConsoleScreen", + "skin": "engine:console", + "contents": { + "type": "relativeLayout", + "contents": [ + { + "type": "RelativeLayout", + "contents": [ + { + "type": "ScrollableArea", + "id": "scrollArea", + "stickToBottom": true, + "content": { + "type": "UIText", + "readOnly": true, + "multiline": true, + "id": "messageHistory", + "family": "history" + }, + "layoutInfo": { + "position-horizontal-center": {}, + "position-top": { + }, + "position-bottom": { + "widget": "commandLine", + "target": "TOP" + } + } + }, + { + "type": "UICommandEntry", + "id": "commandLine", + "family": "commandInput", + "layoutInfo": { + "position-left": {}, + "position-right": {}, + "use-content-height": true, + "position-bottom": {} + } + } + ], + "layoutInfo": { + "position-horizontal-center": {}, + "position-bottom": { + "target": "MIDDLE" + } + } + } + ] + } +} diff --git a/engine/src/main/resources/org/destinationsol/assets/ui/migTestScreen.ui b/engine/src/main/resources/org/destinationsol/assets/ui/migTestScreen.ui new file mode 100644 index 000000000..aa4ba8f2b --- /dev/null +++ b/engine/src/main/resources/org/destinationsol/assets/ui/migTestScreen.ui @@ -0,0 +1,698 @@ +{ + "type": "engine:MigTestScreen", + "skin": "engine:migTestSkin", + "contents": { + "type": "migLayout", + "layoutConstraints": "", + "colConstraints": "[min][pref][min][pref][grow]", + "rowConstraints": "[min]", + "debug": true, + "contents": [ + { + "type": "UILabel", + "id": "", + "text": "", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UILabel", + "id": "", + "text": "disabled", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UILabel", + "id": "", + "text": "min", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UILabel", + "id": "", + "text": "pref", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UILabel", + "id": "", + "text": "grow", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UILabel", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UILabel", + "id": "label1", + "text": "label1", + "layoutInfo": { + "cc": "" + }, + "enabled": false + }, + { + "type": "UILabel", + "id": "label2", + "text": "label2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UILabel", + "id": "label3", + "text": "label3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UILabel", + "id": "label4", + "text": "label4", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UIButton", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UIButton", + "id": "button1", + "text": "button1", + "layoutInfo": { + "cc": "" + }, + "enabled": false + }, + { + "type": "UIButton", + "id": "button2", + "text": "button2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UIButton", + "id": "button3", + "text": "button3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UIButton", + "id": "button4", + "text": "button4", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UISlider", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UISlider", + "id": "slider1", + "layoutInfo": { + "cc": "" + }, + "enabled": false + }, + { + "type": "UISlider", + "id": "slider2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UISlider", + "id": "slider3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UISlider", + "id": "slider4", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UIDoubleSlider", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UIDoubleSlider", + "id": "doubleSlider1", + "layoutInfo": { + "cc": "" + }, + "enabled": false + }, + { + "type": "UIDoubleSlider", + "id": "doubleSlider2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UIDoubleSlider", + "id": "doubleSlider3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UIDoubleSlider", + "id": "doubleSlider4", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UIText", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UIText", + "id": "text1", + "text": "text1", + "layoutInfo": { + "cc": "" + }, + "readOnly": true, + "enabled": false + }, + { + "type": "UIText", + "id": "text2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UIText", + "id": "text3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UIText", + "id": "text4", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UIText with hint", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UIText", + "id": "text1", + "hintText": "Enter text here", + "text": "text1", + "layoutInfo": { + "cc": "" + }, + "readOnly": true, + "enabled": false + }, + { + "type": "UIText", + "id": "text2", + "hintText": "Enter text here", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UIText", + "id": "text3", + "hintText": "Enter text here", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UIText", + "id": "text4", + "hintText": "Enter extra long hint text here that just keeps on going", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "ResettableUIText", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "ResettableUIText", + "id": "rtext1", + "text": "text1", + "layoutInfo": { + "cc": "" + }, + "readOnly": true, + "enabled": false + }, + { + "type": "ResettableUIText", + "id": "rtext2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "ResettableUIText", + "id": "rtext3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "ResettableUIText", + "id": "rtext4", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UIDropdown", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UIDropdown", + "id": "dropdown1", + "layoutInfo": { + "cc": "" + }, + "enabled": false + }, + { + "type": "UIDropdown", + "id": "dropdown2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UIDropdown", + "id": "dropdown3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UIDropdown", + "id": "dropdown4", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UIDropdownScrollable", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UIDropdownScrollable", + "id": "dropdownScrollable1", + "layoutInfo": { + "cc": "" + }, + "enabled": false + }, + { + "type": "UIDropdownScrollable", + "id": "dropdownScrollable2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UIDropdownScrollable", + "id": "dropdownScrollable3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UIDropdownScrollable", + "id": "dropdownScrollable4", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UICheckbox", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UICheckbox", + "id": "checkbox1", + "layoutInfo": { + "cc": "" + }, + "enabled": false + }, + { + "type": "UICheckbox", + "id": "checkbox2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UICheckbox", + "id": "checkbox3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UICheckbox", + "id": "checkbox4", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UIList", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UIList", + "id": "list1", + "layoutInfo": { + "cc": "" + }, + "enabled": false + }, + { + "type": "UIList", + "id": "list2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UIList", + "id": "list3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UIList", + "id": "list4", + "layoutInfo": { + "cc": "grow" + } + }, + { + "type": "UILabel", + "id": "", + "text": "UIBox", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "UIBox", + "id": "box1", + "layoutInfo": { + "cc": "" + }, + "content": { + "type": "ColumnLayout", + "family": "option-grid", + "columns": 2, + "column-widths": [0.35, 0.65], + "verticalSpacing": 8, + "horizontalSpacing": 8, + "layoutInfo": { + "width": 360, + "height": 192, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 16, + "widget": "subtitle" + }, + "position-bottom": { + "target": "TOP", + "offset": 32, + "widget": "close" + } + }, + "contents": [ + { + "type": "UILabel", + "text": "Example" + }, + { + "type": "UISlider" + } + ] + }, + "enabled": false + }, + { + "type": "UIBox", + "id": "box2", + "layoutInfo": { + "cc": "w min, h min" + }, + "content": { + "type": "ColumnLayout", + "family": "option-grid", + "columns": 2, + "column-widths": [0.35, 0.65], + "verticalSpacing": 8, + "horizontalSpacing": 8, + "layoutInfo": { + "width": 360, + "height": 192, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 16, + "widget": "subtitle" + }, + "position-bottom": { + "target": "TOP", + "offset": 32, + "widget": "close" + } + }, + "contents": [ + { + "type": "UILabel", + "text": "Example" + }, + { + "type": "UISlider" + } + ] + } + }, + { + "type": "UIBox", + "id": "box3", + "layoutInfo": { + "cc": "" + }, + "content": { + "type": "ColumnLayout", + "family": "option-grid", + "columns": 2, + "column-widths": [0.35, 0.65], + "verticalSpacing": 8, + "horizontalSpacing": 8, + "layoutInfo": { + "width": 360, + "height": 192, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 16, + "widget": "subtitle" + }, + "position-bottom": { + "target": "TOP", + "offset": 32, + "widget": "close" + } + }, + "contents": [ + { + "type": "UILabel", + "text": "Example" + }, + { + "type": "UISlider" + } + ] + } + }, + { + "type": "UIBox", + "id": "box4", + "layoutInfo": { + "cc": "grow" + }, + "content": { + "type": "ColumnLayout", + "family": "option-grid", + "columns": 2, + "column-widths": [0.35, 0.65], + "verticalSpacing": 8, + "horizontalSpacing": 8, + "layoutInfo": { + "width": 360, + "height": 192, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 16, + "widget": "subtitle" + }, + "position-bottom": { + "target": "TOP", + "offset": 32, + "widget": "close" + } + }, + "contents": [ + { + "type": "UILabel", + "text": "Example" + }, + { + "type": "UISlider" + } + ] + } + }, + { + "type": "UILabel", + "id": "", + "text": "ScrollableArea", + "layoutInfo": { + "cc": "newline" + } + }, + { + "type": "ScrollableArea", + "id": "scrollbar1", + "layoutInfo": { + "width": 70, + "cc": "", + "position-horizontal-center": {} + }, + "enabled": false, + "content": { + "type": "RelativeLayout", + "contents": [ + { + "type": "UILabel", + "id": "message", + "text": "Lorem ipsum, Lorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum", + "layoutInfo": { + "position-left": {}, + "position-right": {}, + "use-content-height": true, + "position-bottom": {} + } + } + ] + } + }, + { + "type": "UIScrollbar", + "id": "scrollbar2", + "layoutInfo": { + "cc": "w min, h min" + } + }, + { + "type": "UIScrollbar", + "id": "scrollbar3", + "layoutInfo": { + "cc": "" + } + }, + { + "type": "UIScrollbar", + "id": "scrollbar4", + "layoutInfo": { + "cc": "grow" + } + } + ] + } +} \ No newline at end of file