/* * 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.terasology.rendering.nui.widgets; import org.terasology.input.MouseInput; import org.terasology.math.TeraMath; import org.terasology.math.geom.Rect2i; import org.terasology.math.geom.Vector2i; import org.terasology.rendering.nui.BaseInteractionListener; import org.terasology.rendering.nui.Canvas; import org.terasology.rendering.nui.CoreWidget; import org.terasology.rendering.nui.InteractionListener; import org.terasology.rendering.nui.LayoutConfig; import org.terasology.rendering.nui.SubRegion; import org.terasology.rendering.nui.databinding.Binding; import org.terasology.rendering.nui.databinding.DefaultBinding; import org.terasology.rendering.nui.events.NUIMouseClickEvent; import org.terasology.rendering.nui.events.NUIMouseDragEvent; import org.terasology.rendering.nui.events.NUIMouseReleaseEvent; /** * A simple slider bar with two handles */ public class UIDoubleSlider extends CoreWidget { public static final String SLIDER_PART = "slider"; public static final String TICKER_LEFT_PART = "tickerLeft"; public static final String TICKER_RIGHT_PART = "tickerRight"; @LayoutConfig private Binding<Float> minimum = new DefaultBinding<>(0.0f); @LayoutConfig private Binding<Float> range = new DefaultBinding<>(1.0f); @LayoutConfig private Binding<Float> increment = new DefaultBinding<>(0.1f); @LayoutConfig private int precision = 1; @LayoutConfig private Binding<Float> valueLeft = new DefaultBinding<>(0.3f); @LayoutConfig private Binding<Float> valueRight = new DefaultBinding<>(0.7f); private int sliderWidth; private String formatString = "0.0"; private boolean active; private InteractionListener tickerListenerLeft = new BaseInteractionListener() { private Vector2i offset = new Vector2i(); @Override public boolean onMouseClick(NUIMouseClickEvent event) { if (event.getMouseButton() == MouseInput.MOUSE_LEFT) { active = true; offset.set(event.getRelativeMousePosition()); offset.x -= pixelOffsetFor(getValueLeft(), sliderWidth); return true; } return false; } @Override public void onMouseRelease(NUIMouseReleaseEvent event) { active = false; } @Override public void onMouseDrag(NUIMouseDragEvent event) { if (sliderWidth > 0) { Vector2i pos = event.getRelativeMousePosition(); int maxSlot = TeraMath.floorToInt(getRange() / getIncrement()); int slotWidth = sliderWidth / maxSlot; int nearestSlot = maxSlot * (pos.x - offset.x + slotWidth / 2) / sliderWidth; nearestSlot = TeraMath.clamp(nearestSlot, 0, maxSlot); float newValue = TeraMath.clamp(getIncrement() * nearestSlot, 0, getRange()) + getMinimum(); setValueLeft(newValue); } } }; private InteractionListener tickerListenerRight = new BaseInteractionListener() { private Vector2i offset = new Vector2i(); @Override public boolean onMouseClick(NUIMouseClickEvent event) { if (event.getMouseButton() == MouseInput.MOUSE_LEFT) { active = true; offset.set(event.getRelativeMousePosition()); offset.x -= pixelOffsetFor(getValueRight(), sliderWidth); return true; } return false; } @Override public void onMouseRelease(NUIMouseReleaseEvent event) { active = false; } @Override public void onMouseDrag(NUIMouseDragEvent event) { if (sliderWidth > 0) { Vector2i pos = event.getRelativeMousePosition(); int maxSlot = TeraMath.floorToInt(getRange() / getIncrement()); int slotWidth = sliderWidth / maxSlot; int nearestSlot = maxSlot * (pos.x - offset.x + slotWidth / 2) / sliderWidth; nearestSlot = TeraMath.clamp(nearestSlot, 0, maxSlot); float newValue = TeraMath.clamp(getIncrement() * nearestSlot, 0, getRange()) + getMinimum(); setValueRight(newValue); } } }; public UIDoubleSlider() { } public UIDoubleSlider(String id) { super(id); } @Override public void onDraw(Canvas canvas) { canvas.setPart(SLIDER_PART); canvas.drawBackground(); drawTicker(canvas, TICKER_LEFT_PART, valueLeft, tickerListenerLeft, false); drawTicker(canvas, TICKER_RIGHT_PART, valueRight, tickerListenerRight, true); } private void drawTicker(Canvas canvas, String part, Binding<Float> value, InteractionListener tickerListener, boolean rightTicker) { canvas.setPart(part); String display = String.format("%." + precision + "f", value.get()); int tickerWidth = canvas.getCurrentStyle().getFont().getWidth(formatString); tickerWidth += canvas.getCurrentStyle().getMargin().getTotalWidth(); sliderWidth = canvas.size().x - tickerWidth * 2; int drawLocation = pixelOffsetFor(value.get(), sliderWidth); if (rightTicker) { drawLocation += tickerWidth; } Rect2i tickerRegion = Rect2i.createFromMinAndSize(drawLocation, 0, tickerWidth, canvas.size().y); try (SubRegion ignored = canvas.subRegion(tickerRegion, false)) { canvas.drawBackground(); canvas.drawText(display); if (isEnabled()) { canvas.addInteractionRegion(tickerListener); } } } @Override public Vector2i getPreferredContentSize(Canvas canvas, Vector2i areaHint) { Vector2i result = new Vector2i(); canvas.setPart(SLIDER_PART); result.x = canvas.getCurrentStyle().getFixedWidth(); if (result.x == 0) { result.x = canvas.getCurrentStyle().getMinWidth(); } result.y = canvas.getCurrentStyle().getFixedHeight(); if (result.y == 0) { result.y = canvas.getCurrentStyle().getMinHeight(); } Vector2i left = getTickerPreferredContentSize(canvas, TICKER_LEFT_PART); Vector2i right = getTickerPreferredContentSize(canvas, TICKER_RIGHT_PART); result.y = Math.max(result.y, Math.max(left.y, right.y)); result.x = Math.max(result.x, left.x + left.y); return result; } private Vector2i getTickerPreferredContentSize(Canvas canvas, String part) { Vector2i result = new Vector2i(); canvas.setPart(part); int tickerWidth = canvas.getCurrentStyle().getFont().getWidth(formatString); tickerWidth += canvas.getCurrentStyle().getMargin().getTotalWidth(); result.x = tickerWidth; if (canvas.getCurrentStyle().getFixedWidth() != 0) { result.x = Math.max(result.x, canvas.getCurrentStyle().getFixedWidth()); } else { result.x = Math.max(result.x, canvas.getCurrentStyle().getMinWidth()); } if (canvas.getCurrentStyle().getFixedHeight() != 0) { result.y = canvas.getCurrentStyle().getFixedHeight(); } else { result.y = canvas.getCurrentStyle().getMinHeight(); } return result; } @Override public String getMode() { if (!isEnabled()) { return DISABLED_MODE; } if (active) { return ACTIVE_MODE; } else if (tickerListenerLeft.isMouseOver() || tickerListenerRight.isMouseOver()) { return HOVER_MODE; } return DEFAULT_MODE; } public void bindMinimum(Binding<Float> binding) { this.minimum = binding; } /** * @return A Float indicating the minimum value. */ public float getMinimum() { return minimum.get(); } /** * @param min A Float indicating the minimum value settable. */ public void setMinimum(float min) { this.minimum.set(min); generateFormatString(); } public void bindRange(Binding<Float> binding) { this.range = binding; } /** * @return A Float indicating the range of values. */ public float getRange() { return range.get(); } /** * @param val A Float specifying the range of values. */ public void setRange(float val) { range.set(val); generateFormatString(); } public void bindIncrement(Binding<Float> binding) { increment = binding; } /** * @return A Float indicating the smallest increment. */ public float getIncrement() { return increment.get(); } /** * @param val A Float specifying the smallest increment. */ public void setIncrement(float val) { increment.set(val); } public void bindValueLeft(Binding<Float> binding) { valueLeft = binding; } public void bindValueRight(Binding<Float> binding) { valueRight = binding; } /** * @return A Float containing the value of the left handle. */ public float getValueLeft() { return valueLeft.get(); } /** * @param val The new value of the left handle */ public void setValueLeft(float val) { valueLeft.set(val); if (val > valueRight.get()) { valueRight.set(val); } } /** * @return A Float containing the value of the right handle. */ public float getValueRight() { return valueRight.get(); } /** * @param val The new value of the right handle. */ public void setValueRight(float val) { valueRight.set(val); if (val < valueLeft.get()) { valueLeft.set(val); } } /** * @return The number of decimal points displayed. */ public int getPrecision() { return precision; } /** * @param precision The number of decimal points to display. */ public void setPrecision(int precision) { this.precision = precision; generateFormatString(); } private void generateFormatString() { float maxValue = getRange() + getMinimum(); int leadingValues = String.format("%.0f", maxValue).length(); StringBuilder newFormat = new StringBuilder(); if (getMinimum() < 0) { newFormat.append('-'); } for (int i = 0; i < leadingValues; ++i) { newFormat.append('0'); } if (precision > 0) { newFormat.append('.'); for (int i = 0; i < precision; ++i) { newFormat.append('0'); } } formatString = newFormat.toString(); } private int pixelOffsetFor(float val, int width) { return TeraMath.floorToInt(width * (val - getMinimum()) / getRange()); } }