Added Menus and cursor visibility management

- Layers now have a CursorPolicy
  - Used to enable/disable cursor based on top layer
- Added a default menu layer implementation
This commit is contained in:
OLEGSHA 2021-06-28 17:45:49 +03:00
parent 085f602427
commit eace6733ce
Signed by: OLEGSHA
GPG Key ID: E57A4B08D64AFF7A
14 changed files with 376 additions and 92 deletions

View File

@ -24,6 +24,7 @@ import java.util.List;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface;
import ru.windcorp.progressia.client.graphics.input.CursorEvent; import ru.windcorp.progressia.client.graphics.input.CursorEvent;
import ru.windcorp.progressia.client.graphics.input.FrameResizeEvent; import ru.windcorp.progressia.client.graphics.input.FrameResizeEvent;
import ru.windcorp.progressia.client.graphics.input.InputEvent; import ru.windcorp.progressia.client.graphics.input.InputEvent;
@ -57,15 +58,24 @@ public class GUI {
} }
public static void addBottomLayer(Layer layer) { public static void addBottomLayer(Layer layer) {
modify(layers -> layers.add(layer)); modify(layers -> {
layers.add(layer);
layer.onAdded();
});
} }
public static void addTopLayer(Layer layer) { public static void addTopLayer(Layer layer) {
modify(layers -> layers.add(0, layer)); modify(layers -> {
layers.add(0, layer);
layer.onAdded();
});
} }
public static void removeLayer(Layer layer) { public static void removeLayer(Layer layer) {
modify(layers -> layers.remove(layer)); modify(layers -> {
layers.remove(layer);
layer.onRemoved();
});
} }
private static void modify(LayerStackModification mod) { private static void modify(LayerStackModification mod) {
@ -78,12 +88,33 @@ public class GUI {
public static void render() { public static void render() {
synchronized (LAYERS) { synchronized (LAYERS) {
if (!MODIFICATION_QUEUE.isEmpty()) {
MODIFICATION_QUEUE.forEach(action -> action.affect(LAYERS)); MODIFICATION_QUEUE.forEach(action -> action.affect(LAYERS));
MODIFICATION_QUEUE.clear(); MODIFICATION_QUEUE.clear();
boolean isMouseCurrentlyCaptured = GraphicsInterface.isMouseCaptured();
Layer.CursorPolicy policy = Layer.CursorPolicy.REQUIRE;
for (Layer layer : LAYERS) {
Layer.CursorPolicy currentPolicy = layer.getCursorPolicy();
if (currentPolicy != Layer.CursorPolicy.INDIFFERENT) {
policy = currentPolicy;
break;
}
}
boolean shouldCaptureMouse = (policy == Layer.CursorPolicy.FORBID);
if (shouldCaptureMouse != isMouseCurrentlyCaptured) {
GraphicsInterface.setMouseCaptured(shouldCaptureMouse);
}
}
for (int i = LAYERS.size() - 1; i >= 0; --i) { for (int i = LAYERS.size() - 1; i >= 0; --i) {
LAYERS.get(i).render(); LAYERS.get(i).render();
} }
} }
} }

View File

@ -31,15 +31,52 @@ public abstract class Layer {
private final AtomicBoolean isValid = new AtomicBoolean(false); private final AtomicBoolean isValid = new AtomicBoolean(false);
/**
* Represents various requests that a {@link Layer} can make regarding the
* presence of a visible cursor. The value of the highest layer that is not
* {@link #INDIFFERENT} is used.
*/
public static enum CursorPolicy {
/**
* Require that a cursor is visible.
*/
REQUIRE,
/**
* The {@link Layer} should not affect the presence or absence of a
* visible cursor; lower layers should be consulted.
*/
INDIFFERENT,
/**
* Forbid a visible cursor.
*/
FORBID
}
private CursorPolicy cursorPolicy = CursorPolicy.INDIFFERENT;
public Layer(String name) { public Layer(String name) {
this.name = name; this.name = name;
} }
public String getName() {
return name;
}
@Override @Override
public String toString() { public String toString() {
return "Layer " + name; return "Layer " + name;
} }
public CursorPolicy getCursorPolicy() {
return cursorPolicy;
}
public void setCursorPolicy(CursorPolicy cursorPolicy) {
this.cursorPolicy = cursorPolicy;
}
void render() { void render() {
GraphicsInterface.startNextLayer(); GraphicsInterface.startNextLayer();
@ -79,4 +116,12 @@ public abstract class Layer {
return GraphicsInterface.getFrameHeight(); return GraphicsInterface.getFrameHeight();
} }
protected void onAdded() {
// Do nothing
}
protected void onRemoved() {
// Do nothing
}
} }

View File

@ -192,4 +192,18 @@ public class GraphicsBackend {
GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor()); GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
return vidmode.refreshRate(); return vidmode.refreshRate();
} }
public static boolean isMouseCaptured() {
return glfwGetInputMode(windowHandle, GLFW_CURSOR) == GLFW_CURSOR_DISABLED;
}
public static void setMouseCaptured(boolean capture) {
int mode = capture ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL;
glfwSetInputMode(windowHandle, GLFW_CURSOR, mode);
if (!capture) {
glfwSetCursorPos(windowHandle, FRAME_SIZE.x / 2.0, FRAME_SIZE.y / 2.0);
}
}
} }

View File

@ -82,4 +82,12 @@ public class GraphicsInterface {
GraphicsBackend.setVSyncEnabled(GraphicsBackend.isVSyncEnabled()); GraphicsBackend.setVSyncEnabled(GraphicsBackend.isVSyncEnabled());
} }
public static boolean isMouseCaptured() {
return GraphicsBackend.isMouseCaptured();
}
public static void setMouseCaptured(boolean capture) {
GraphicsBackend.setMouseCaptured(capture);
}
} }

View File

@ -65,8 +65,6 @@ class LWJGLInitializer {
GraphicsBackend.setWindowHandle(handle); GraphicsBackend.setWindowHandle(handle);
glfwSetInputMode(handle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
glfwMakeContextCurrent(handle); glfwMakeContextCurrent(handle);
glfwSwapInterval(0); // TODO: remove after config system is added glfwSwapInterval(0); // TODO: remove after config system is added
} }

View File

@ -17,19 +17,64 @@
*/ */
package ru.windcorp.progressia.client.graphics.gui; package ru.windcorp.progressia.client.graphics.gui;
import java.util.Objects;
import glm.vec._4.Vec4;
import ru.windcorp.progressia.client.graphics.Colors; import ru.windcorp.progressia.client.graphics.Colors;
import ru.windcorp.progressia.client.graphics.flat.RenderTarget; import ru.windcorp.progressia.client.graphics.flat.RenderTarget;
public class Panel extends Group { public class Panel extends Group {
public Panel(String name, Layout layout) { private Vec4 fill;
private Vec4 border;
public Panel(String name, Layout layout, Vec4 fill, Vec4 border) {
super(name, layout); super(name, layout);
this.fill = Objects.requireNonNull(fill, "fill");
this.border = border;
}
public Panel(String name, Layout layout) {
this(name, layout, Colors.WHITE, Colors.LIGHT_GRAY);
}
/**
* @return the fill
*/
public Vec4 getFill() {
return fill;
}
/**
* @param fill the fill to set
*/
public void setFill(Vec4 fill) {
this.fill = Objects.requireNonNull(fill, "fill");
}
/**
* @return the border
*/
public Vec4 getBorder() {
return border;
}
/**
* @param border the border to set
*/
public void setBorder(Vec4 border) {
this.border = border;
} }
@Override @Override
protected void assembleSelf(RenderTarget target) { protected void assembleSelf(RenderTarget target) {
target.fill(getX(), getY(), getWidth(), getHeight(), Colors.LIGHT_GRAY); if (border == null) {
target.fill(getX() + 2, getY() + 2, getWidth() - 4, getHeight() - 4, Colors.WHITE); target.fill(getX(), getY(), getWidth(), getHeight(), fill);
} else {
target.fill(getX(), getY(), getWidth(), getHeight(), border);
target.fill(getX() + 2, getY() + 2, getWidth() - 4, getHeight() - 4, fill);
}
} }
} }

View File

@ -0,0 +1,78 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.client.graphics.gui.layout;
import static java.lang.Math.max;
import glm.vec._2.i.Vec2i;
import ru.windcorp.progressia.client.graphics.gui.Component;
import ru.windcorp.progressia.client.graphics.gui.Layout;
public class LayoutFill implements Layout {
private final int margin;
public LayoutFill(int margin) {
this.margin = margin;
}
public LayoutFill() {
this(0);
}
@Override
public void layout(Component c) {
c.getChildren().forEach(child -> {
int cWidth = c.getWidth() - 2 * margin;
int cHeight = c.getHeight() - 2 * margin;
child.setBounds(
c.getX() + margin,
c.getY() + margin,
cWidth,
cHeight
);
});
}
@Override
public Vec2i calculatePreferredSize(Component c) {
Vec2i result = new Vec2i(0, 0);
c.getChildren().stream()
.map(child -> child.getPreferredSize())
.forEach(size -> {
result.x = max(size.x, result.x);
result.y = max(size.y, result.y);
});
result.x += 2 * margin;
result.y += 2 * margin;
return result;
}
@Override
public String toString() {
return getClass().getSimpleName() + "(" + margin + ")";
}
}

View File

@ -0,0 +1,117 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.client.graphics.gui.menu;
import org.lwjgl.glfw.GLFW;
import glm.vec._2.i.Vec2i;
import ru.windcorp.progressia.client.graphics.Colors;
import ru.windcorp.progressia.client.graphics.GUI;
import ru.windcorp.progressia.client.graphics.font.Font;
import ru.windcorp.progressia.client.graphics.gui.Component;
import ru.windcorp.progressia.client.graphics.gui.GUILayer;
import ru.windcorp.progressia.client.graphics.gui.Label;
import ru.windcorp.progressia.client.graphics.gui.Layout;
import ru.windcorp.progressia.client.graphics.gui.Panel;
import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign;
import ru.windcorp.progressia.client.graphics.gui.layout.LayoutFill;
import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical;
import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.client.graphics.input.KeyEvent;
import ru.windcorp.progressia.client.graphics.input.bus.Input;
import ru.windcorp.progressia.client.localization.MutableString;
import ru.windcorp.progressia.client.localization.MutableStringLocalized;
public class MenuLayer extends GUILayer {
private final Component content;
private final Component background;
private final Runnable closeAction = () -> {
GUI.removeLayer(this);
};
public MenuLayer(String name, Component content) {
super(name, new LayoutFill(0));
setCursorPolicy(CursorPolicy.REQUIRE);
this.background = new Panel(name + ".Background", new LayoutAlign(10), Colors.toVector(0x66000000), null);
this.content = content;
background.addChild(content);
getRoot().addChild(background);
}
public MenuLayer(String name, Layout contentLayout) {
this(name, new Panel(name + ".Content", contentLayout));
}
public MenuLayer(String name) {
this(name, new LayoutVertical(20, 10));
}
public Component getContent() {
return content;
}
public Component getBackground() {
return background;
}
protected void addTitle() {
String translationKey = "Layer" + getName() + ".Title";
MutableString titleText = new MutableStringLocalized(translationKey);
Font titleFont = new Font().deriveBold().withColor(Colors.BLACK).withAlign(0.5f);
Label label = new Label(getName() + ".Title", titleFont, titleText);
getContent().addChild(label);
Panel panel = new Panel(getName() + ".Title.Underscore", null, Colors.BLUE, null);
panel.setLayout(new LayoutFill() {
@Override
public Vec2i calculatePreferredSize(Component c) {
return new Vec2i(label.getPreferredSize().x + 40, 4);
}
});
getContent().addChild(panel);
}
protected Runnable getCloseAction() {
return closeAction;
}
@Override
protected void handleInput(Input input) {
if (!input.isConsumed()) {
InputEvent event = input.getEvent();
if (event instanceof KeyEvent) {
KeyEvent keyEvent = (KeyEvent) event;
if (keyEvent.isPress() && keyEvent.getKey() == GLFW.GLFW_KEY_ESCAPE) {
getCloseAction().run();
}
}
}
super.handleInput(input);
input.consume();
}
}

View File

@ -57,6 +57,8 @@ public class LayerWorld extends Layer {
super("World"); super("World");
this.client = client; this.client = client;
this.inputBasedControls = new InputBasedControls(client); this.inputBasedControls = new InputBasedControls(client);
setCursorPolicy(CursorPolicy.FORBID);
} }
@Override @Override

View File

@ -17,47 +17,29 @@
*/ */
package ru.windcorp.progressia.test; package ru.windcorp.progressia.test;
import org.lwjgl.glfw.GLFW;
import ru.windcorp.progressia.client.graphics.Colors; import ru.windcorp.progressia.client.graphics.Colors;
import ru.windcorp.progressia.client.graphics.GUI; import ru.windcorp.progressia.client.graphics.font.Font;
import ru.windcorp.progressia.client.graphics.backend.GraphicsBackend;
import ru.windcorp.progressia.client.graphics.flat.RenderTarget;
import ru.windcorp.progressia.client.graphics.gui.Button; import ru.windcorp.progressia.client.graphics.gui.Button;
import ru.windcorp.progressia.client.graphics.gui.Checkbox; import ru.windcorp.progressia.client.graphics.gui.Checkbox;
import ru.windcorp.progressia.client.graphics.gui.GUILayer; import ru.windcorp.progressia.client.graphics.gui.Label;
import ru.windcorp.progressia.client.graphics.gui.Group;
import ru.windcorp.progressia.client.graphics.gui.Panel;
import ru.windcorp.progressia.client.graphics.gui.RadioButton; import ru.windcorp.progressia.client.graphics.gui.RadioButton;
import ru.windcorp.progressia.client.graphics.gui.RadioButtonGroup; import ru.windcorp.progressia.client.graphics.gui.RadioButtonGroup;
import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; import ru.windcorp.progressia.client.graphics.gui.menu.MenuLayer;
import ru.windcorp.progressia.client.graphics.gui.layout.LayoutBorderHorizontal;
import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical;
import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.client.graphics.input.KeyEvent;
import ru.windcorp.progressia.client.graphics.input.bus.Input;
public class LayerButtonTest extends GUILayer { public class LayerButtonTest extends MenuLayer {
public LayerButtonTest() { public LayerButtonTest() {
super("LayerButtonTest", new LayoutBorderHorizontal(0)); super("ButtonTest");
Group background = new Group("Background", new LayoutAlign(10)) { addTitle();
@Override
protected void assembleSelf(RenderTarget target) {
target.fill(Colors.toVector(0x88FFFFFF));
}
};
Panel panel = new Panel("Panel", new LayoutVertical(10));
Button blockableButton; Button blockableButton;
panel.addChild((blockableButton = new Button("BlockableButton", "Blockable")).addAction(b -> { getContent().addChild((blockableButton = new Button("BlockableButton", "Blockable")).addAction(b -> {
System.out.println("Button Blockable!"); System.out.println("Button Blockable!");
})); }));
blockableButton.setEnabled(false); blockableButton.setEnabled(false);
panel.addChild(new Checkbox("EnableButton", "Enable").addAction(b -> { getContent().addChild(new Checkbox("EnableButton", "Enable").addAction(b -> {
blockableButton.setEnabled(((Checkbox) b).isChecked()); blockableButton.setEnabled(((Checkbox) b).isChecked());
})); }));
@ -65,35 +47,24 @@ public class LayerButtonTest extends GUILayer {
System.out.println("RBG! " + g.getSelected().getLabel().getCurrentText()); System.out.println("RBG! " + g.getSelected().getLabel().getCurrentText());
}); });
panel.addChild(new RadioButton("RB1", "Moon").setGroup(group)); getContent().addChild(new RadioButton("RB1", "Moon").setGroup(group));
panel.addChild(new RadioButton("RB2", "Type").setGroup(group)); getContent().addChild(new RadioButton("RB2", "Type").setGroup(group));
panel.addChild(new RadioButton("RB3", "Ice").setGroup(group)); getContent().addChild(new RadioButton("RB3", "Ice").setGroup(group));
panel.addChild(new RadioButton("RB4", "Cream").setGroup(group)); getContent().addChild(new RadioButton("RB4", "Cream").setGroup(group));
panel.getChild(panel.getChildren().size() - 1).setEnabled(false); getContent().getChild(getContent().getChildren().size() - 1).setEnabled(false);
panel.getChild(1).takeFocus(); getContent().addChild(new Label("Hint", new Font().withColor(Colors.LIGHT_GRAY), "This is a MenuLayer"));
background.addChild(panel); getContent().addChild(new Button("Continue", "Continue").addAction(b -> {
getRoot().addChild(background.setLayoutHint(LayoutBorderHorizontal.CENTER)); getCloseAction().run();
} }));
@Override getContent().addChild(new Button("Quit", "Quit").addAction(b -> {
protected void handleInput(Input input) { System.exit(0);
}));
if (!input.isConsumed()) { getContent().takeFocus();
InputEvent e = input.getEvent();
if ((e instanceof KeyEvent) && ((KeyEvent) e).isPress() && ((KeyEvent) e).getKey() == GLFW.GLFW_KEY_ESCAPE) {
GUI.removeLayer(this);
GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED);
}
}
super.handleInput(input);
input.consume();
} }
} }

View File

@ -73,14 +73,6 @@ public class LayerTestGUI extends GUILayer {
) )
); );
group.addChild(
new Label(
"IsMouseCapturedDisplay",
font,
tmp_dynFormat("LayerTestGUI.IsMouseCapturedDisplay", tpc::isMouseCaptured)
)
);
group.addChild( group.addChild(
new Label( new Label(
"CameraModeDisplay", "CameraModeDisplay",

View File

@ -82,7 +82,6 @@ public class TestPlayerControls {
private double lastSpacePress = Double.NEGATIVE_INFINITY; private double lastSpacePress = Double.NEGATIVE_INFINITY;
private double lastSprintPress = Double.NEGATIVE_INFINITY; private double lastSprintPress = Double.NEGATIVE_INFINITY;
private boolean captureMouse = true;
private boolean useMinecraftGravity = false; private boolean useMinecraftGravity = false;
private int selectedBlock = 0; private int selectedBlock = 0;
@ -294,21 +293,10 @@ public class TestPlayerControls {
} }
private void handleEscape() { private void handleEscape() {
// if (captureMouse) {
// GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL);
// } else {
// GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED);
// }
//
// captureMouse = !captureMouse;
movementForward = 0; movementForward = 0;
movementRight = 0; movementRight = 0;
movementUp = 0; movementUp = 0;
GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL);
GUI.addTopLayer(new LayerButtonTest()); GUI.addTopLayer(new LayerButtonTest());
updateGUI();
} }
private void handleDebugLayerSwitch() { private void handleDebugLayerSwitch() {
@ -352,9 +340,6 @@ public class TestPlayerControls {
} }
private void onMouseMoved(CursorMoveEvent event) { private void onMouseMoved(CursorMoveEvent event) {
if (!captureMouse)
return;
if (ClientState.getInstance() == null || !ClientState.getInstance().isReady()) { if (ClientState.getInstance() == null || !ClientState.getInstance().isReady()) {
return; return;
} }
@ -445,10 +430,6 @@ public class TestPlayerControls {
return isSprinting; return isSprinting;
} }
public boolean isMouseCaptured() {
return captureMouse;
}
public boolean useMinecraftGravity() { public boolean useMinecraftGravity() {
return useMinecraftGravity; return useMinecraftGravity;
} }

View File

@ -6,7 +6,6 @@ LayerAbout.DebugHint = Debug GUI: F3
LayerTestGUI.IsFlyingDisplay = Flying: %5s (Space bar x2) LayerTestGUI.IsFlyingDisplay = Flying: %5s (Space bar x2)
LayerTestGUI.IsSprintingDisplay = Sprinting: %5s (W x2) LayerTestGUI.IsSprintingDisplay = Sprinting: %5s (W x2)
LayerTestGUI.IsMouseCapturedDisplay = Mouse captured: %5s (Esc)
LayerTestGUI.CameraModeDisplay = Camera mode: %5d (F5) LayerTestGUI.CameraModeDisplay = Camera mode: %5d (F5)
LayerTestGUI.GravityModeDisplay = Gravity: %9s (G) LayerTestGUI.GravityModeDisplay = Gravity: %9s (G)
LayerTestGUI.LanguageDisplay = Language: %5s (L) LayerTestGUI.LanguageDisplay = Language: %5s (L)
@ -22,3 +21,5 @@ LayerTestGUI.SelectedTileDisplay = %s Tile: %s
LayerTestGUI.PlacementModeHint = (Blocks %s Tiles: Ctrl + Mouse Wheel) LayerTestGUI.PlacementModeHint = (Blocks %s Tiles: Ctrl + Mouse Wheel)
LayerTestGUI.IsFullscreen = Fullscreen: %5s (F11) LayerTestGUI.IsFullscreen = Fullscreen: %5s (F11)
LayerTestGUI.IsVSync = VSync: %5s (F12) LayerTestGUI.IsVSync = VSync: %5s (F12)
LayerButtonTest.Title = Button Test

View File

@ -6,7 +6,6 @@ LayerAbout.DebugHint = Отладочный GUI: F3
LayerTestGUI.IsFlyingDisplay = Полёт: %5s (Пробел x2) LayerTestGUI.IsFlyingDisplay = Полёт: %5s (Пробел x2)
LayerTestGUI.IsSprintingDisplay = Бег: %5s (W x2) LayerTestGUI.IsSprintingDisplay = Бег: %5s (W x2)
LayerTestGUI.IsMouseCapturedDisplay = Захват мыши: %5s (Esc)
LayerTestGUI.CameraModeDisplay = Камера: %5d (F5) LayerTestGUI.CameraModeDisplay = Камера: %5d (F5)
LayerTestGUI.GravityModeDisplay = Гравитация: %9s (G) LayerTestGUI.GravityModeDisplay = Гравитация: %9s (G)
LayerTestGUI.LanguageDisplay = Язык: %5s (L) LayerTestGUI.LanguageDisplay = Язык: %5s (L)
@ -22,3 +21,5 @@ LayerTestGUI.SelectedTileDisplay = %s Плитка: %s
LayerTestGUI.PlacementModeHint = (Блок %s плитки: Ctrl + прокрутка) LayerTestGUI.PlacementModeHint = (Блок %s плитки: Ctrl + прокрутка)
LayerTestGUI.IsFullscreen = Полный экран: %5s (F11) LayerTestGUI.IsFullscreen = Полный экран: %5s (F11)
LayerTestGUI.IsVSync = Верт. синхр.: %5s (F12) LayerTestGUI.IsVSync = Верт. синхр.: %5s (F12)
LayerButtonTest.Title = Тест Кнопок