├── .gitignore ├── LICENSE ├── README.md ├── example ├── build.gradle.kts └── src │ └── main │ ├── java │ └── org │ │ └── meteordev │ │ └── example │ │ ├── Calculator.java │ │ ├── Main.java │ │ ├── Module.java │ │ ├── Window.java │ │ └── musicplayer │ │ ├── MusicPlayer.java │ │ ├── Player.java │ │ └── Song.java │ └── resources │ ├── calculator │ ├── Comfortaa.ttf │ ├── buttons.pts │ └── theme.pts │ ├── meteor │ ├── Comfortaa.ttf │ ├── dropdown.svg │ ├── reset.svg │ └── theme.pts │ ├── pulsar-player │ ├── Comfortaa.ttf │ ├── play.svg │ └── theme.pts │ └── white-red │ ├── Comfortaa.ttf │ ├── Roboto.ttf │ ├── dropdown.pts │ ├── edits.pts │ ├── global.pts │ ├── icons │ ├── check.svg │ ├── click.svg │ ├── down.svg │ └── write.svg │ ├── slider.pts │ └── theme.pts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── properties.json ├── pts-intellij ├── build.gradle.kts └── src │ └── main │ ├── java │ └── org │ │ └── meteordev │ │ └── pts │ │ ├── PtsASTFactory.java │ │ ├── PtsBraceMatcher.java │ │ ├── PtsColorLineMarkerProvider.java │ │ ├── PtsCommenter.java │ │ ├── PtsElementColorProvider.java │ │ ├── PtsElementFactory.java │ │ ├── PtsFileType.java │ │ ├── PtsFoldingBuilder.java │ │ ├── PtsLanguage.java │ │ ├── PtsParserDefinition.java │ │ ├── PtsQuoteHandler.java │ │ ├── completion │ │ ├── Completions.java │ │ ├── PtsCharFilter.java │ │ └── PtsCompletionContributor.java │ │ ├── highlight │ │ ├── PtsHighlighter.java │ │ ├── PtsSyntaxHighlighter.java │ │ └── PtsSyntaxHighlighterFactory.java │ │ ├── psi │ │ ├── PtsAtStatement.java │ │ ├── PtsAtVar.java │ │ ├── PtsColor.java │ │ ├── PtsFunction.java │ │ ├── PtsPSIFileRoot.java │ │ ├── PtsProperty.java │ │ ├── PtsPsiNode.java │ │ └── PtsStyle.java │ │ └── structure │ │ ├── PtsStructureAwareNavBar.java │ │ ├── PtsStructureViewElement.java │ │ ├── PtsStructureViewFactory.java │ │ └── PtsStructureViewModel.java │ └── resources │ └── META-INF │ └── plugin.xml ├── pts-vscode ├── .eslintrc.json ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── .yarnrc ├── README.md ├── language-configuration.json ├── package.json ├── src │ └── extension.ts ├── syntaxes │ └── pts.tmLanguage.json ├── tsconfig.json └── yarn.lock ├── pts ├── build.gradle.kts └── src │ └── main │ ├── antlr │ └── Pts.g4 │ └── java │ └── org │ └── meteordev │ └── pts │ ├── properties │ ├── Properties.java │ ├── Property.java │ ├── PropertyAccessor.java │ ├── PropertyConstructor.java │ ├── PropertyType.java │ ├── PropertyTypes.java │ └── ValueType.java │ └── utils │ ├── AlignX.java │ ├── AlignY.java │ ├── Color4.java │ ├── ColorFactory.java │ ├── ColorImpl.java │ ├── IColor.java │ ├── ListDirection.java │ ├── Overflow.java │ ├── Vec2.java │ └── Vec4.java ├── pulsar ├── build.gradle.kts └── src │ └── main │ ├── java │ └── org │ │ └── meteordev │ │ └── pulsar │ │ ├── input │ │ ├── CharTypedEvent.java │ │ ├── Event.java │ │ ├── EventHandler.java │ │ ├── EventType.java │ │ ├── KeyEvent.java │ │ ├── MouseButtonEvent.java │ │ ├── MouseMovedEvent.java │ │ ├── MouseScrolledEvent.java │ │ └── UsableEvent.java │ │ ├── layout │ │ ├── BasicLayout.java │ │ ├── HorizontalLayout.java │ │ ├── Layout.java │ │ ├── MaxSizeCalculationContext.java │ │ ├── TableLayout.java │ │ └── VerticalLayout.java │ │ ├── rendering │ │ ├── CharData.java │ │ ├── DebugRenderer.java │ │ ├── Font.java │ │ ├── FontInfo.java │ │ ├── Fonts.java │ │ ├── Icons.java │ │ ├── Renderer.java │ │ ├── TextureAtlas.java │ │ └── TextureRegion.java │ │ ├── theme │ │ ├── IStylable.java │ │ ├── Selector.java │ │ ├── Style.java │ │ ├── Styles.java │ │ ├── Theme.java │ │ ├── fileresolvers │ │ │ ├── IFileResolver.java │ │ │ ├── NormalFileResolver.java │ │ │ └── ResourceFileResolver.java │ │ └── parser │ │ │ ├── ParseException.java │ │ │ └── Parser.java │ │ ├── utils │ │ ├── CharFilters.java │ │ ├── ICharFilter.java │ │ ├── IntStack.java │ │ ├── Lists.java │ │ ├── Matrix.java │ │ ├── PropertyMap.java │ │ └── Utils.java │ │ └── widgets │ │ ├── Cell.java │ │ ├── WButton.java │ │ ├── WCheckbox.java │ │ ├── WContainer.java │ │ ├── WDoubleEdit.java │ │ ├── WDropdown.java │ │ ├── WHorizontalList.java │ │ ├── WHorizontalSeparator.java │ │ ├── WIcon.java │ │ ├── WImage.java │ │ ├── WIntEdit.java │ │ ├── WPressable.java │ │ ├── WRoot.java │ │ ├── WSection.java │ │ ├── WSlider.java │ │ ├── WTable.java │ │ ├── WText.java │ │ ├── WTextBox.java │ │ ├── WTexture.java │ │ ├── WVerticalList.java │ │ ├── WWindow.java │ │ ├── WWindowManager.java │ │ └── Widget.java │ └── resources │ └── pulsar │ └── shaders │ ├── basic.frag │ ├── basic.vert │ ├── icon.frag │ ├── rectangles.frag │ ├── rectangles.vert │ ├── text.frag │ ├── texture.frag │ └── texture.vert ├── settings.gradle.kts └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | 3 | .idea/* 4 | !.idea/copyright/* 5 | 6 | **/build 7 | 8 | *.log 9 | 10 | pts/src/generated/** -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pulsar 2 | GUI framework for Java using LWJGL3. -------------------------------------------------------------------------------- /example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java") 3 | id("application") 4 | } 5 | 6 | group = "org.meteordev" 7 | version = "0.1.0" 8 | var mainClassName = "$group.example.musicplayer.MusicPlayer" 9 | 10 | repositories { 11 | mavenCentral() 12 | 13 | maven { 14 | name = "Meteor - Snapshots" 15 | setUrl("https://maven.meteordev.org/snapshots") 16 | } 17 | } 18 | 19 | val lwjglNatives = Pair( 20 | System.getProperty("os.name")!!, 21 | System.getProperty("os.arch")!! 22 | ).let { (name, arch) -> 23 | when { 24 | arrayOf("Linux", "FreeBSD", "SunOS", "Unit").any { name.startsWith(it) } -> 25 | if (arrayOf("arm", "aarch64").any { arch.startsWith(it) }) 26 | "natives-linux${if (arch.contains("64") || arch.startsWith("armv8")) "-arm64" else "-arm32"}" 27 | else 28 | "natives-linux" 29 | arrayOf("Mac OS X", "Darwin").any { name.startsWith(it) } -> 30 | "natives-macos${if (arch.startsWith("aarch64")) "-arm64" else ""}" 31 | arrayOf("Windows").any { name.startsWith(it) } -> 32 | if (arch.contains("64")) 33 | "natives-windows${if (arch.startsWith("aarch64")) "-arm64" else ""}" 34 | else 35 | "natives-windows-x86" 36 | else -> throw Error("Unrecognized or unsupported platform. Please set \"lwjglNatives\" manually") 37 | } 38 | } 39 | 40 | dependencies { 41 | implementation(project(":pulsar")) 42 | 43 | implementation("com.udojava:EvalEx:2.7") 44 | implementation("com.badlogicgames.jlayer:jlayer:1.0.2-gdx") 45 | implementation("com.mpatric:mp3agic:0.9.1") 46 | 47 | implementation("org.meteordev:juno-api:0.1.0-SNAPSHOT") 48 | implementation("org.meteordev:juno-opengl:0.1.0-SNAPSHOT") 49 | 50 | implementation(platform("org.lwjgl:lwjgl-bom:3.3.1")) 51 | 52 | implementation("org.lwjgl:lwjgl") 53 | implementation("org.lwjgl:lwjgl-glfw") 54 | implementation("org.lwjgl:lwjgl-opengl") 55 | implementation("org.lwjgl:lwjgl-stb") 56 | implementation("org.lwjgl:lwjgl-nanovg") 57 | implementation("org.lwjgl:lwjgl-openal") 58 | implementation("org.joml:joml:1.10.2") 59 | 60 | runtimeOnly("org.lwjgl:lwjgl::$lwjglNatives") 61 | runtimeOnly("org.lwjgl:lwjgl-glfw::$lwjglNatives") 62 | runtimeOnly("org.lwjgl:lwjgl-opengl::$lwjglNatives") 63 | runtimeOnly("org.lwjgl:lwjgl-stb::$lwjglNatives") 64 | runtimeOnly("org.lwjgl:lwjgl-nanovg::$lwjglNatives") 65 | runtimeOnly("org.lwjgl:lwjgl-openal::$lwjglNatives") 66 | } 67 | 68 | tasks.withType { 69 | sourceCompatibility = JavaVersion.VERSION_17.toString() 70 | targetCompatibility = JavaVersion.VERSION_17.toString() 71 | } 72 | 73 | tasks.withType { 74 | dependsOn(project(":pulsar").tasks.withType()) 75 | 76 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 77 | 78 | manifest { 79 | attributes("Main-Class" to mainClassName) 80 | } 81 | 82 | from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) 83 | } 84 | -------------------------------------------------------------------------------- /example/src/main/java/org/meteordev/example/Calculator.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.example; 2 | 3 | import com.udojava.evalex.Expression; 4 | import org.meteordev.pulsar.rendering.Renderer; 5 | import org.meteordev.pulsar.theme.Theme; 6 | import org.meteordev.pulsar.theme.fileresolvers.ResourceFileResolver; 7 | import org.meteordev.pulsar.theme.parser.Parser; 8 | import org.meteordev.pulsar.widgets.*; 9 | 10 | import static org.lwjgl.glfw.GLFW.glfwGetTime; 11 | import static org.lwjgl.opengl.GL11C.*; 12 | 13 | public class Calculator { 14 | private static WText text; 15 | 16 | private static void button(WTable table, String number, String tag) { 17 | WButton button = table.add(new WButton(number)).expandX().widget(); 18 | button.action = () -> text.setText(text.getText() + number); 19 | 20 | if (tag != null) button.tag(tag); 21 | } 22 | 23 | private static void button(WTable table, String number) { 24 | button(table, number, null); 25 | } 26 | 27 | private static Widget create() { 28 | WVerticalList list = new WVerticalList(); 29 | 30 | text = list.add(new WText("")).widget(); 31 | 32 | WTable table = list.add(new WTable()).expandX().widget(); 33 | 34 | WButton bA = table.add(new WButton("A")).expandX().tag("secondary").widget(); 35 | WButton bC = table.add(new WButton("C")).expandX().tag("secondary").widget(); 36 | bC.action = () -> text.setText(""); 37 | WButton bRemove = table.add(new WButton("<-")).expandX().tag("secondary").widget(); 38 | bRemove.action = () -> { 39 | if (text.getText().length() > 0) text.setText(text.getText().substring(0, text.getText().length() - 1)); 40 | }; 41 | button(table, "/", "secondary"); 42 | 43 | table.row(); 44 | 45 | button(table, "7"); 46 | button(table, "8"); 47 | button(table, "9"); 48 | button(table, "*", "secondary"); 49 | 50 | table.row(); 51 | 52 | button(table, "4"); 53 | button(table, "5"); 54 | button(table, "6"); 55 | button(table, "-", "secondary"); 56 | 57 | table.row(); 58 | 59 | button(table, "1"); 60 | button(table, "2"); 61 | button(table, "3"); 62 | button(table, "+", "secondary"); 63 | 64 | table.row(); 65 | 66 | button(table, "-"); 67 | button(table, "0"); 68 | button(table, "."); 69 | WButton bEquals = table.add(new WButton("=")).expandX().tag("equals").widget(); 70 | bEquals.action = () -> { 71 | Expression expression = new Expression(text.getText()); 72 | text.setText(expression.eval().toString()); 73 | }; 74 | 75 | return list; 76 | } 77 | 78 | public static void main(String[] args) { 79 | Window window = new Window("Calculator", 321, 331); 80 | Renderer renderer = new Renderer(); 81 | 82 | Theme theme = Parser.parse(new ResourceFileResolver("/calculator"), "theme.pts"); 83 | renderer.setTheme(theme); 84 | renderer.window = window.handle; 85 | 86 | WRoot root = new WRoot(false); 87 | root.setWindowSize(window.getWidth(), window.getHeight()); 88 | 89 | root.add(create()).expandX(); 90 | 91 | window.onResized = root::setWindowSize; 92 | window.onEvent = root::dispatch; 93 | 94 | double lastTime = glfwGetTime(); 95 | 96 | while (!window.shouldClose()) { 97 | double time = glfwGetTime(); 98 | double delta = time - lastTime; 99 | lastTime = time; 100 | 101 | window.pollEvents(); 102 | 103 | glClearColor(0.3f, 0.3f, 0.3f, 1); 104 | glClear(GL_COLOR_BUFFER_BIT); 105 | 106 | root.render(renderer, delta); 107 | 108 | window.swapBuffers(); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /example/src/main/java/org/meteordev/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.example; 2 | 3 | import org.meteordev.pts.properties.Properties; 4 | import org.meteordev.pts.utils.Overflow; 5 | import org.meteordev.pulsar.rendering.Renderer; 6 | import org.meteordev.pulsar.theme.Theme; 7 | import org.meteordev.pulsar.theme.fileresolvers.ResourceFileResolver; 8 | import org.meteordev.pulsar.theme.parser.Parser; 9 | import org.meteordev.pts.utils.AlignX; 10 | import org.meteordev.pulsar.widgets.*; 11 | 12 | import static org.lwjgl.glfw.GLFW.glfwGetTime; 13 | import static org.lwjgl.opengl.GL11C.*; 14 | 15 | public class Main { 16 | private static WWindow createMainWindow() { 17 | WWindow w = new WWindow("Test Window"); 18 | 19 | w.add(new WText("Hello")); 20 | w.add(new WText("COPE?!?!?!!?")).right(); 21 | w.add(new WHorizontalSeparator("Something")); 22 | 23 | WTable t = w.add(new WTable()).expandX().widget(); 24 | 25 | t.add(new WText("Something:")); 26 | t.add(new WDropdown<>(AlignX.Center)); 27 | t.row(); 28 | 29 | t.add(new WText("Good:")); 30 | t.add(new WCheckbox(true)); 31 | t.row(); 32 | 33 | t.add(new WText("Text:")); 34 | t.add(new WTextBox("Cope?")).expandX(); 35 | t.row(); 36 | 37 | t.add(new WHorizontalSeparator("Numbers")); 38 | t.row(); 39 | 40 | t.add(new WText("Integer 1:")); 41 | t.add(new WIntEdit(2, -8, 8, -8, 8)); 42 | t.row(); 43 | 44 | t.add(new WText("Integer 2:")); 45 | t.add(new WIntEdit(2, null, null)); 46 | t.row(); 47 | 48 | t.add(new WText("Double 1:")); 49 | t.add(new WDoubleEdit(2, -8d, 8d, -8, 8)); 50 | t.row(); 51 | 52 | t.add(new WText("Double 2:")); 53 | t.add(new WDoubleEdit(2, null, null)); 54 | t.row(); 55 | 56 | w.add(new WHorizontalSeparator()); 57 | 58 | w.add(new WButton("Click me")).expandX(); 59 | 60 | return w; 61 | } 62 | 63 | private static WWindow createLoginWindow() { 64 | WWindow w = new WWindow("Login"); 65 | WTable t = w.add(new WTable()).expandX().widget(); 66 | 67 | t.add(new WText("Username:")); 68 | t.add(new WTextBox("", "Username")).expandX(); 69 | t.row(); 70 | 71 | t.add(new WText("Password:")); 72 | t.add(new WTextBox("", "Password", '*')).expandX(); 73 | 74 | w.add(new WButton("Login")).expandX(); 75 | 76 | return w; 77 | } 78 | 79 | private static WWindow createLongWindow() { 80 | WWindow w = new WWindow("Long"); 81 | w.bodySet(Properties.MAX_HEIGHT, 200.0); 82 | w.bodySet(Properties.OVERFLOW_Y, Overflow.Scroll); 83 | 84 | for (int i = 0; i < 20; i++) { 85 | w.add(new WText("Item: " + i)); 86 | } 87 | 88 | return w; 89 | } 90 | 91 | public static void main(String[] args) { 92 | Window window = new Window("GUI Example", 1280, 720); 93 | Renderer renderer = new Renderer(); 94 | 95 | Theme theme = Parser.parse(new ResourceFileResolver("/white-red"), "theme.pts"); 96 | renderer.setTheme(theme); 97 | renderer.window = window.handle; 98 | 99 | WRoot root = new WRoot(false); 100 | root.setWindowSize(window.getWidth(), window.getHeight()); 101 | 102 | WWindowManager windows = root.add(new WWindowManager()).expandX().widget(); 103 | 104 | windows.add(createMainWindow()); 105 | windows.add(createLoginWindow()); 106 | windows.add(createLongWindow()); 107 | 108 | window.onResized = root::setWindowSize; 109 | window.onEvent = root::dispatch; 110 | 111 | double lastTime = glfwGetTime(); 112 | 113 | while (!window.shouldClose()) { 114 | double time = glfwGetTime(); 115 | double delta = time - lastTime; 116 | lastTime = time; 117 | 118 | window.pollEvents(); 119 | 120 | glClearColor(0.9f, 0.9f, 0.9f, 1); 121 | glClear(GL_COLOR_BUFFER_BIT); 122 | 123 | root.render(renderer, delta); 124 | 125 | window.swapBuffers(); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /example/src/main/java/org/meteordev/example/Module.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.example; 2 | 3 | import org.meteordev.pulsar.layout.TableLayout; 4 | import org.meteordev.pulsar.rendering.Renderer; 5 | import org.meteordev.pulsar.theme.Theme; 6 | import org.meteordev.pulsar.theme.fileresolvers.ResourceFileResolver; 7 | import org.meteordev.pulsar.theme.parser.Parser; 8 | import org.meteordev.pulsar.widgets.*; 9 | 10 | import static org.lwjgl.glfw.GLFW.glfwGetTime; 11 | import static org.lwjgl.opengl.GL11C.*; 12 | 13 | public class Module { 14 | public enum Mode { 15 | Default, 16 | Copium, 17 | Beta 18 | } 19 | 20 | private static WWindow createWindow() { 21 | WWindow w = new WWindow("Module", false); 22 | 23 | w.add(new WText("Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quam ab impedit ratione in voluptates autem suscipit veritatis labore?")).tag("description"); 24 | 25 | // General 26 | WSection s = w.add(new WSection("General")).expandX().widget(); 27 | TableLayout t = s.setLayout(new TableLayout()); 28 | 29 | s.add(new WText("Mode")); 30 | s.add(new WDropdown<>(Mode.Default)).expandCellX(); 31 | s.add(new WButton(null)).tag("reset"); 32 | t.row(); 33 | 34 | s.add(new WText("Toggle Perspective")); 35 | s.add(new WCheckbox(true)).expandCellX(); 36 | s.add(new WButton(null)).tag("reset"); 37 | t.row(); 38 | 39 | s.add(new WText("Speed")); 40 | s.add(new WDoubleEdit(47.481, 0.0, null, 0, 100)).expandX(); 41 | s.add(new WButton(null)).tag("reset"); 42 | t.row(); 43 | 44 | // Active 45 | w.add(new WHorizontalSeparator()); 46 | WHorizontalList l = w.add(new WHorizontalList()).expandX().widget(); 47 | l.add(new WText("Active")); 48 | l.add(new WCheckbox(false)); 49 | 50 | return w; 51 | } 52 | 53 | public static void main(String[] args) { 54 | Window window = new Window("GUI Example", 1280, 720); 55 | Renderer renderer = new Renderer(); 56 | 57 | Theme theme = Parser.parse(new ResourceFileResolver("/meteor"), "theme.pts"); 58 | renderer.setTheme(theme); 59 | renderer.window = window.handle; 60 | 61 | WRoot root = new WRoot(true); 62 | root.setWindowSize(window.getWidth(), window.getHeight()); 63 | 64 | WWindowManager windows = root.add(new WWindowManager()).expandX().widget(); 65 | 66 | windows.add(createWindow()); 67 | 68 | window.onResized = root::setWindowSize; 69 | window.onEvent = root::dispatch; 70 | 71 | double lastTime = glfwGetTime(); 72 | 73 | while (!window.shouldClose()) { 74 | double time = glfwGetTime(); 75 | double delta = time - lastTime; 76 | lastTime = time; 77 | 78 | window.pollEvents(); 79 | 80 | glClearColor(0.9f, 0.9f, 0.9f, 1); 81 | glClear(GL_COLOR_BUFFER_BIT); 82 | 83 | root.render(renderer, delta); 84 | 85 | window.swapBuffers(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/src/main/java/org/meteordev/example/Window.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.example; 2 | 3 | import org.lwjgl.opengl.GL; 4 | import org.meteordev.juno.api.JunoProvider; 5 | import org.meteordev.juno.opengl.GLJuno; 6 | import org.meteordev.pulsar.input.*; 7 | 8 | import java.util.function.BiConsumer; 9 | import java.util.function.Consumer; 10 | 11 | import static org.lwjgl.glfw.GLFW.*; 12 | import static org.lwjgl.opengl.GL11C.*; 13 | 14 | public class Window { 15 | public final long handle; 16 | private int width, height; 17 | public double lastMouseX, lastMouseY; 18 | 19 | private final MouseButtonEvent mouseButtonEvent = new MouseButtonEvent(); 20 | private final MouseMovedEvent mouseMovedEvent = new MouseMovedEvent(); 21 | private final MouseScrolledEvent mouseScrolledEvent = new MouseScrolledEvent(); 22 | private final KeyEvent keyEvent = new KeyEvent(); 23 | private final CharTypedEvent charTypedEvent = new CharTypedEvent(); 24 | 25 | public BiConsumer onResized; 26 | public Consumer onEvent; 27 | 28 | public Window(String title, int width, int height) { 29 | glfwInit(); 30 | 31 | this.width = width; 32 | this.height = height; 33 | 34 | glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); 35 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 36 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); 37 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 38 | handle = glfwCreateWindow(width, height, title, 0, 0); 39 | 40 | glfwMakeContextCurrent(handle); 41 | GL.createCapabilities(); 42 | 43 | glEnable(GL_BLEND); 44 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 45 | 46 | glfwSetWindowSizeCallback(handle, (window, width1, height1) -> { 47 | this.width = width1; 48 | this.height = height1; 49 | 50 | glViewport(0, 0, width1, height1); 51 | 52 | if (onResized != null) onResized.accept(this.width, this.height); 53 | }); 54 | 55 | glfwSetMouseButtonCallback(handle, (window, button, action, mods) -> { 56 | if (onEvent != null) onEvent.accept(mouseButtonEvent.set(action == GLFW_RELEASE ? EventType.MouseReleased : EventType.MousePressed, lastMouseX, lastMouseY, button)); 57 | }); 58 | 59 | glfwSetCursorPosCallback(handle, (window, xpos, ypos) -> { 60 | if (onEvent != null) onEvent.accept(mouseMovedEvent.set(xpos, ypos, xpos - lastMouseX, ypos - lastMouseY)); 61 | 62 | lastMouseX = xpos; 63 | lastMouseY = ypos; 64 | }); 65 | 66 | glfwSetScrollCallback(handle, (window, xoffset, yoffset) -> { 67 | if (onEvent != null) onEvent.accept(mouseScrolledEvent.set(yoffset)); 68 | }); 69 | 70 | glfwSetKeyCallback(handle, (window, key, scancode, action, mods) -> { 71 | if (onEvent != null) { 72 | if (action == GLFW_PRESS) onEvent.accept(keyEvent.set(EventType.KeyPressed, key, mods)); 73 | else if (action == GLFW_REPEAT) onEvent.accept(keyEvent.set(EventType.KeyRepeated, key, mods)); 74 | } 75 | }); 76 | 77 | glfwSetCharCallback(handle, (window, codepoint) -> { 78 | if (onEvent != null) onEvent.accept(charTypedEvent.set((char) codepoint)); 79 | }); 80 | 81 | glfwSwapInterval(1); 82 | glfwShowWindow(handle); 83 | 84 | JunoProvider.set(new GLJuno()); 85 | } 86 | 87 | public boolean shouldClose() { 88 | return glfwWindowShouldClose(handle); 89 | } 90 | 91 | public void pollEvents() { 92 | glfwPollEvents(); 93 | } 94 | 95 | public void swapBuffers() { 96 | glfwSwapBuffers(handle); 97 | } 98 | 99 | public int getWidth() { 100 | return width; 101 | } 102 | 103 | public int getHeight() { 104 | return height; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /example/src/main/java/org/meteordev/example/musicplayer/MusicPlayer.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.example.musicplayer; 2 | 3 | import org.meteordev.example.Window; 4 | import org.meteordev.pulsar.rendering.Renderer; 5 | import org.meteordev.pulsar.theme.Theme; 6 | import org.meteordev.pulsar.theme.fileresolvers.ResourceFileResolver; 7 | import org.meteordev.pulsar.theme.parser.Parser; 8 | import org.meteordev.pulsar.widgets.*; 9 | 10 | import static org.lwjgl.glfw.GLFW.glfwGetTime; 11 | import static org.lwjgl.opengl.GL11C.*; 12 | 13 | public class MusicPlayer { 14 | public static boolean containsIgnoreCase(String str, String searchStr) { 15 | if (str == null || searchStr == null) return false; 16 | 17 | final int length = searchStr.length(); 18 | if (length == 0) 19 | return true; 20 | 21 | for (int i = str.length() - length; i >= 0; i--) { 22 | if (str.regionMatches(true, i, searchStr, 0, length)) 23 | return true; 24 | } 25 | 26 | return false; 27 | } 28 | 29 | private static void fillSongs(Widget list, String searchText) { 30 | for (Song song : Player.SONGS) { 31 | if (!containsIgnoreCase(song.getSearchString(), searchText)) continue; 32 | 33 | WHorizontalList songW = list.add(new WHorizontalList()).tag("song").expandX().widget(); 34 | 35 | songW.add(new WImage(song.getImage(), song.getImageWidth(), song.getImageHeight())); 36 | 37 | WVerticalList textW = songW.add(new WVerticalList()).expandX().widget(); 38 | textW.add(new WText(song.getTitle())).tag("title"); 39 | textW.add(new WText(song.getArtist())).tag("author"); 40 | 41 | WButton play = songW.add(new WButton(null)).tag("play").widget(); 42 | play.checkIcon(); 43 | play.action = () -> Player.play(song); 44 | } 45 | } 46 | 47 | private static WVerticalList songs; 48 | private static WSlider progress; 49 | 50 | private static Widget createControls() { 51 | WVerticalList list = new WVerticalList(); 52 | list.tag("controls"); 53 | 54 | list.add(new WHorizontalSeparator()).expandX(); 55 | 56 | WHorizontalList top = list.add(new WHorizontalList()).expandX().widget(); 57 | WHorizontalList bottom = list.add(new WHorizontalList()).expandX().widget(); 58 | 59 | progress = bottom.add(new WSlider(0, 0, 1)).expandX().widget(); 60 | 61 | return list; 62 | } 63 | 64 | private static Widget create() { 65 | WVerticalList list = new WVerticalList(); 66 | list.tag("main"); 67 | 68 | WVerticalList top = list.add(new WVerticalList()).tag("songs").expandX().widget(); 69 | WTextBox searchBar = top.add(new WTextBox("", "Search")).expandX().widget(); 70 | top.add(new WHorizontalSeparator()).expandX(); 71 | 72 | songs = list.add(new WVerticalList()).tag("songs").expandX().widget(); 73 | 74 | searchBar.setFocused(true); 75 | searchBar.action = () -> { 76 | songs.clear(); 77 | fillSongs(songs, searchBar.get()); 78 | }; 79 | 80 | fillSongs(songs, searchBar.get()); 81 | 82 | list.add(createControls()).expandX(); 83 | 84 | return list; 85 | } 86 | 87 | public static void main(String[] args) { 88 | Player.load(); 89 | 90 | Window window = new Window("Pulsar Player", 600, 400); 91 | Renderer renderer = new Renderer(); 92 | 93 | Theme theme = Parser.parse(new ResourceFileResolver("/pulsar-player"), "theme.pts"); 94 | renderer.setTheme(theme); 95 | renderer.window = window.handle; 96 | 97 | WRoot root = new WRoot(true); 98 | root.setWindowSize(window.getWidth(), window.getHeight()); 99 | 100 | root.add(create()).expandX(); 101 | 102 | window.onResized = root::setWindowSize; 103 | window.onEvent = root::dispatch; 104 | 105 | double lastTime = glfwGetTime(); 106 | 107 | while (!window.shouldClose()) { 108 | double time = glfwGetTime(); 109 | double delta = time - lastTime; 110 | lastTime = time; 111 | 112 | window.pollEvents(); 113 | 114 | glClearColor(0.3f, 0.3f, 0.3f, 1); 115 | glClear(GL_COLOR_BUFFER_BIT); 116 | 117 | progress.set(Player.getProgress()); 118 | 119 | root.render(renderer, delta); 120 | 121 | window.swapBuffers(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /example/src/main/java/org/meteordev/example/musicplayer/Player.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.example.musicplayer; 2 | 3 | import org.lwjgl.openal.AL; 4 | import org.lwjgl.openal.ALC; 5 | import org.lwjgl.openal.ALC10; 6 | import org.lwjgl.openal.ALCCapabilities; 7 | 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | import java.nio.IntBuffer; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.util.ArrayList; 14 | import java.util.Comparator; 15 | import java.util.List; 16 | 17 | import static org.lwjgl.openal.AL10.*; 18 | import static org.lwjgl.openal.AL11.AL_SEC_OFFSET; 19 | import static org.lwjgl.openal.ALC10.alcMakeContextCurrent; 20 | import static org.lwjgl.openal.ALC10.alcOpenDevice; 21 | 22 | public class Player { 23 | public static final List SONGS = new ArrayList<>(); 24 | 25 | private static Song SONG; 26 | private static int SOURCE; 27 | private static int BUFFER; 28 | 29 | public static void load() { 30 | long device = alcOpenDevice((ByteBuffer) null); 31 | ALCCapabilities alcCaps = ALC.createCapabilities(device); 32 | 33 | long context = ALC10.alcCreateContext(device, (IntBuffer) null); 34 | alcMakeContextCurrent(context); 35 | AL.createCapabilities(alcCaps); 36 | 37 | SOURCE = alGenSources(); 38 | alSourcef(SOURCE, AL_GAIN, 1); 39 | 40 | try { 41 | Files.find(Path.of(System.getProperty("user.home"), "Music"), 10, (path, basicFileAttributes) -> basicFileAttributes.isRegularFile()) 42 | .filter(path -> path.getFileName().toString().endsWith(".mp3")) 43 | .map(Song::new) 44 | .sorted(Comparator.comparing(Song::getSearchString)) 45 | .limit(5) 46 | .forEach(SONGS::add); 47 | } catch (IOException e) { 48 | throw new RuntimeException(e); 49 | } 50 | 51 | System.out.println("Loaded " + SONGS.size() + " songs"); 52 | } 53 | 54 | public static void play(Song song) { 55 | if (alGetSourcei(SOURCE, AL_SOURCE_STATE) == AL_PLAYING) { 56 | alSourceStop(SOURCE); 57 | alDeleteBuffers(BUFFER); 58 | } 59 | 60 | SONG = song; 61 | System.out.format("Playing %s by %s%n", song.getTitle(), song.getArtist()); 62 | 63 | BUFFER = song.loadBuffer(); 64 | alSourcei(SOURCE, AL_BUFFER, BUFFER); 65 | alSourcePlay(SOURCE); 66 | } 67 | 68 | public static double getProgress() { 69 | if (SONG == null) return 0; 70 | 71 | int position = alGetSourcei(SOURCE, AL_SEC_OFFSET); 72 | return (double) position / SONG.getSeconds(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /example/src/main/resources/calculator/Comfortaa.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeteorDevelopment/pulsar/462bb05a1a803e78e67b4142bda1b094748ed8ad/example/src/main/resources/calculator/Comfortaa.ttf -------------------------------------------------------------------------------- /example/src/main/resources/calculator/buttons.pts: -------------------------------------------------------------------------------- 1 | // Primary 2 | 3 | button { 4 | padding: 16px; 5 | background-color: #000000; 6 | } 7 | 8 | button :hovered { 9 | background-color: #0a0a0a; 10 | } 11 | 12 | button :pressed { 13 | background-color: #141414; 14 | } 15 | 16 | // Secondary 17 | 18 | button .secondary { 19 | background-color: #282828; 20 | } 21 | 22 | button .secondary :hovered { 23 | background-color: #323232; 24 | } 25 | 26 | button .secondary :pressed { 27 | background-color: #3c3c3c; 28 | } 29 | 30 | // Equals 31 | 32 | button .equals { 33 | background-color: #288c8c; 34 | } 35 | 36 | button .equals :hovered { 37 | background-color: #329696; 38 | } 39 | 40 | button .equals :pressed { 41 | background-color: #3ca0a0; 42 | } -------------------------------------------------------------------------------- /example/src/main/resources/calculator/theme.pts: -------------------------------------------------------------------------------- 1 | @title "Calculator"; 2 | @authors [ "MineGame159" ]; 3 | 4 | @include "buttons.pts"; 5 | 6 | root { 7 | padding: 2px; 8 | } 9 | 10 | text { 11 | font: "Comfortaa.ttf"; 12 | font-size: 24px; 13 | align-x: center; 14 | color: #FFFFFF; 15 | } 16 | 17 | table { 18 | spacing: 2px; 19 | } 20 | 21 | vertical-list { 22 | spacing: 8px; 23 | align-y: top; 24 | } -------------------------------------------------------------------------------- /example/src/main/resources/meteor/Comfortaa.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeteorDevelopment/pulsar/462bb05a1a803e78e67b4142bda1b094748ed8ad/example/src/main/resources/meteor/Comfortaa.ttf -------------------------------------------------------------------------------- /example/src/main/resources/meteor/dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 42 | 44 | 48 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /example/src/main/resources/meteor/reset.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/src/main/resources/meteor/theme.pts: -------------------------------------------------------------------------------- 1 | @title "Meteor"; 2 | @authors [ "MineGame159" ]; 3 | 4 | @var accent: Color4 = rgb(145, 61, 226); 5 | @var bg: Color4 = rgba(20, 20, 20, 200); 6 | @var bgH: Color4 = rgba(30, 30, 30, 200); 7 | @var bgP: Color4 = rgba(40, 40, 40, 200); 8 | @var ol: Color4 = rgba(0, 0, 0, 255); 9 | @var olH: Color4 = rgb(10, 10, 10); 10 | @var olP: Color4 = rgb(20, 20, 20); 11 | 12 | text { 13 | font: "Comfortaa.ttf"; 14 | font-size: 18px; 15 | color: #FFFFFF; 16 | } 17 | 18 | text .description { 19 | max-width: 1000px; 20 | } 21 | 22 | vertical-list { 23 | spacing: 2px; 24 | } 25 | 26 | horizontal-list { 27 | spacing: 6px; 28 | } 29 | 30 | table { 31 | spacing: 6px; 32 | } 33 | 34 | // Window 35 | 36 | window { 37 | max-width: 700px; 38 | } 39 | 40 | window-header { 41 | background-color: !accent; 42 | padding: 6px; 43 | radius: 6px; 44 | } 45 | 46 | window-header .expanded { 47 | radius: 6px 6px 0px 0px; 48 | } 49 | 50 | .window-title { 51 | font-size: 22px; 52 | align-x: center; 53 | } 54 | 55 | window-body { 56 | background-color: !bg; 57 | padding: 10px; 58 | radius: 0px 0px 6px 6px; 59 | } 60 | 61 | // Horizontal Separator 62 | 63 | horizontal-separator { 64 | background-color: #FFFFFF; 65 | size: 1px; 66 | spacing: 4px; 67 | padding: 8px 0px 8px 0px; 68 | } 69 | 70 | horizontal-separator .has-text { 71 | padding: 0px; 72 | } 73 | 74 | horizontal-separator-text { 75 | align-x: center; 76 | } 77 | 78 | // Dropdown 79 | 80 | dropdown { 81 | background-color: !bg; 82 | outline-size: 4px; 83 | outline-color: !ol; 84 | padding: 6px; 85 | spacing: 6px 4px; 86 | icon: "dropdown"; 87 | list-direction: reversed; 88 | radius: 2px; 89 | } 90 | 91 | dropdown :hovered { 92 | background-color: !bgH; 93 | outline-color: !olH; 94 | } 95 | 96 | dropdown :pressed { 97 | background-color: !bgP; 98 | outline-color: !olP; 99 | } 100 | 101 | .dropdown-text { 102 | align-x: center; 103 | } 104 | 105 | icon .dropdown { 106 | icon-path: "dropdown.svg"; 107 | size: 18px; 108 | color: #FFFFFF; 109 | } 110 | 111 | .dropdown-body { 112 | outline-size: 4px; 113 | outline-color: !ol; 114 | spacing: 0px; 115 | radius: 2px; 116 | } 117 | 118 | .dropdown-value { 119 | background-color: !bg; 120 | padding: 6px; 121 | } 122 | 123 | .dropdown-value :hovered { 124 | background-color: !bgH; 125 | } 126 | 127 | .dropdown-value :pressed { 128 | background-color: !bgP; 129 | } 130 | 131 | .dropdown-value-text { 132 | align-x: center; 133 | } 134 | 135 | // Checkbox 136 | 137 | checkbox { 138 | background-color: !bg; 139 | outline-size: 4px; 140 | outline-color: !ol; 141 | padding: 6px; 142 | radius: 2px; 143 | } 144 | 145 | checkbox :hovered { 146 | background-color: !bgH; 147 | outline-color: !olH; 148 | } 149 | 150 | checkbox :pressed { 151 | background-color: !bgP; 152 | outline-color: !olP; 153 | } 154 | 155 | checkbox-inner { 156 | background-color: !accent; 157 | size: 18px; 158 | } 159 | 160 | // Text Box 161 | 162 | text-box { 163 | background-color: !bg; 164 | outline-size: 4px; 165 | outline-color: !ol; 166 | padding: 6px; 167 | color: #FFFFFF; 168 | radius: 2px; 169 | } 170 | 171 | selection { 172 | background-color: rgba(45, 125, 245, 100); 173 | } 174 | 175 | // Slider 176 | 177 | .slider-left { 178 | background-color: rgb(100, 35, 170); 179 | size: 4px; 180 | radius: 2px; 181 | } 182 | 183 | .slider-right { 184 | background-color: rgb(50, 50, 50); 185 | size: 4px; 186 | radius: 2px; 187 | } 188 | 189 | slider-handle { 190 | background-color: rgb(130, 0, 255); 191 | size: 18px; 192 | radius: 9px; 193 | } 194 | 195 | slider-handle :hovered { 196 | background-color: rgb(140, 30, 255); 197 | } 198 | 199 | slider-handle :pressed { 200 | background-color: rgb(150, 60, 255); 201 | } 202 | 203 | // Double Edit 204 | 205 | text-box .double-edit { 206 | minimum-size: 80px 0px; 207 | } 208 | 209 | // Button 210 | 211 | button { 212 | background-color: !bg; 213 | outline-size: 4px; 214 | outline-color: !ol; 215 | padding: 6px; 216 | icon: "reset"; 217 | radius: 2px; 218 | } 219 | 220 | button :hovered { 221 | background-color: !bgH; 222 | outline-color: !olH; 223 | } 224 | 225 | button :pressed { 226 | background-color: !bgP; 227 | outline-color: !olP; 228 | } 229 | 230 | icon .reset { 231 | icon-path: "reset.svg"; 232 | size: 18px; 233 | color: #FFFFFF; 234 | } 235 | -------------------------------------------------------------------------------- /example/src/main/resources/pulsar-player/Comfortaa.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeteorDevelopment/pulsar/462bb05a1a803e78e67b4142bda1b094748ed8ad/example/src/main/resources/pulsar-player/Comfortaa.ttf -------------------------------------------------------------------------------- /example/src/main/resources/pulsar-player/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | play_arrow 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/src/main/resources/pulsar-player/theme.pts: -------------------------------------------------------------------------------- 1 | @title "Pulsar Player"; 2 | @authors [ "MineGame159" ]; 3 | 4 | root { 5 | align-y: top; 6 | } 7 | 8 | text { 9 | font: "Comfortaa.ttf"; 10 | font-size: 16px; 11 | color: #FFFFFF; 12 | } 13 | 14 | vertical-list { 15 | spacing: 3px; 16 | } 17 | 18 | vertical-list .main { 19 | padding: 6px; 20 | padding.right: 0px; 21 | } 22 | 23 | vertical-list .songs { 24 | padding.right: 6px; 25 | overflow-y: scroll; 26 | } 27 | 28 | horizontal-separator { 29 | size: 1px; 30 | padding.vertical: 6px; 31 | background-color: #FFFFFF; 32 | } 33 | 34 | text-box { 35 | color: #FFFFFF; 36 | } 37 | 38 | text-box-placeholder { 39 | color: rgb(200, 200, 200); 40 | } 41 | 42 | selection { 43 | background-color: rgba(45, 125, 245, 100); 44 | radius: 2px; 45 | } 46 | 47 | scrollbar { 48 | padding: 2px; 49 | padding.left: 0px; 50 | } 51 | 52 | scrollbar-handle { 53 | size.x: 6px; 54 | background-color: #FFFFFF; 55 | radius: 2px; 56 | } 57 | 58 | scrollbar-handle :hovered { 59 | background-color: rgb(200, 200, 200); 60 | } 61 | 62 | .song { 63 | spacing: 4px; 64 | } 65 | 66 | image { 67 | size: 50px 50px; 68 | } 69 | 70 | .author { 71 | font-size: 14px; 72 | color: rgb(200, 200, 200); 73 | } 74 | 75 | button .play { 76 | icon: "play"; 77 | } 78 | 79 | icon .play { 80 | icon-path: "play.svg"; 81 | size: 20px; 82 | color: #FFFFFF; 83 | } 84 | 85 | icon .play :hovered { 86 | color: rgb(225, 225, 225); 87 | } 88 | 89 | icon .play :pressed { 90 | color: rgb(200, 200, 200); 91 | } 92 | 93 | .controls { 94 | padding.right: 6px; 95 | } 96 | 97 | .slider-left { 98 | size: 6px; 99 | radius: 3px; 100 | background-color: #B84242; 101 | } 102 | 103 | .slider-right { 104 | size: 6px; 105 | radius: 3px; 106 | background-color: #2C1616; 107 | } 108 | 109 | slider-handle { 110 | size: 18px; 111 | radius: 9px; 112 | background-color: #9ACE17; 113 | } 114 | -------------------------------------------------------------------------------- /example/src/main/resources/white-red/Comfortaa.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeteorDevelopment/pulsar/462bb05a1a803e78e67b4142bda1b094748ed8ad/example/src/main/resources/white-red/Comfortaa.ttf -------------------------------------------------------------------------------- /example/src/main/resources/white-red/Roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeteorDevelopment/pulsar/462bb05a1a803e78e67b4142bda1b094748ed8ad/example/src/main/resources/white-red/Roboto.ttf -------------------------------------------------------------------------------- /example/src/main/resources/white-red/dropdown.pts: -------------------------------------------------------------------------------- 1 | dropdown { 2 | @apply outline; 3 | 4 | background-color: !bg; 5 | padding: 4px 6px 4px 6px; 6 | spacing: 6px 4px; 7 | icon: "down"; 8 | list-direction: reversed; 9 | } 10 | 11 | dropdown :hovered { 12 | background-color: !bg-hovered; 13 | } 14 | 15 | dropdown :pressed { 16 | background-color: !bg-pressed; 17 | } 18 | 19 | icon .down { 20 | icon-path: "icons/down.svg"; 21 | size: 20px; 22 | color: #000000; 23 | } 24 | 25 | icon .down :hovered { 26 | color: rgb(20, 20, 20); 27 | } 28 | 29 | icon .down :pressed { 30 | color: rgb(40, 40, 40); 31 | } 32 | 33 | .dropdown-text { 34 | align-x: center; 35 | } 36 | 37 | .dropdown-body { 38 | @apply outline; 39 | 40 | spacing: 0px; 41 | } 42 | 43 | .dropdown-value { 44 | background-color: !bg; 45 | padding: 4px; 46 | } 47 | 48 | .dropdown-value :hovered { 49 | background-color: !bg-hovered; 50 | } 51 | 52 | .dropdown-value :pressed { 53 | background-color: !bg-pressed; 54 | } 55 | 56 | .dropdown-value-text { 57 | align-x: center; 58 | } -------------------------------------------------------------------------------- /example/src/main/resources/white-red/edits.pts: -------------------------------------------------------------------------------- 1 | // Int edit 2 | 3 | int-edit { 4 | spacing: 4px; 5 | } 6 | 7 | text-box .int-edit { 8 | minimum-size: 100px 0px; 9 | } 10 | 11 | button .int-edit { 12 | icon: "none"; 13 | minimum-size: 28px; 14 | } 15 | 16 | button-text .int-edit { 17 | font-size: 20px; 18 | align-x: center; 19 | } 20 | 21 | // Double edit 22 | 23 | double-edit { 24 | spacing: 4px; 25 | } 26 | 27 | text-box .double-edit { 28 | minimum-size: 100px 0px; 29 | } 30 | 31 | button .double-edit { 32 | icon: "none"; 33 | minimum-size: 28px; 34 | } 35 | 36 | button-text .double-edit { 37 | font-size: 20px; 38 | align-x: center; 39 | } -------------------------------------------------------------------------------- /example/src/main/resources/white-red/global.pts: -------------------------------------------------------------------------------- 1 | @var text: Color = rgb(15, 15, 15); 2 | 3 | @var window-header: Color4 = rgb(255, 60, 60) rgb(255, 100, 100) rgb(255, 100, 100) rgb(255, 60, 60); 4 | @var window-body: Color = #FFFFFF; 5 | 6 | @var bg: Color = rgb(240, 240, 240); 7 | @var bg-hovered: Color = rgb(230, 230, 230); 8 | @var bg-pressed: Color = rgb(220, 220, 220); 9 | 10 | @mixin window-outline { 11 | outline-size: 2px; 12 | outline-color: #000000; 13 | } 14 | 15 | @mixin outline { 16 | outline-size: 2px; 17 | outline-color: rgb(127, 127, 127); 18 | } -------------------------------------------------------------------------------- /example/src/main/resources/white-red/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /example/src/main/resources/white-red/icons/click.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon 27 one finger click 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/main/resources/white-red/icons/down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/src/main/resources/white-red/icons/write.svg: -------------------------------------------------------------------------------- 1 | Write Message -------------------------------------------------------------------------------- /example/src/main/resources/white-red/slider.pts: -------------------------------------------------------------------------------- 1 | slider { 2 | padding: 0px; 3 | minimum-size: 220px 0px; 4 | } 5 | 6 | .slider-left { 7 | background-color: rgb(255, 60, 60); 8 | outline-size: 2px; 9 | outline-color: rgb(245, 50, 50); 10 | size: 6px; 11 | radius: 3px; 12 | } 13 | 14 | .slider-right { 15 | background-color: rgb(240, 240, 240); 16 | outline-size: 2px; 17 | outline-color: rgb(230, 230, 230); 18 | size: 6px; 19 | radius: 3px; 20 | } 21 | 22 | slider-handle { 23 | background-color: rgb(255, 20, 20); 24 | outline-size: 2px; 25 | outline-color: rgb(200, 20, 20); 26 | size: 16px; 27 | radius: 8px; 28 | } 29 | 30 | slider-handle :hovered { 31 | background-color: rgb(255, 50, 50); 32 | outline-color: rgb(200, 40, 40); 33 | } 34 | 35 | slider-handle :pressed { 36 | background-color: rgb(255, 70, 70); 37 | outline-color: rgb(200, 60, 60); 38 | } -------------------------------------------------------------------------------- /example/src/main/resources/white-red/theme.pts: -------------------------------------------------------------------------------- 1 | @title "White Red"; 2 | @authors [ "MineGame159", "Coper" ]; 3 | 4 | @include "global.pts"; 5 | @include "slider.pts"; 6 | @include "dropdown.pts"; 7 | @include "edits.pts"; 8 | 9 | widget { 10 | spacing: 6px; 11 | radius: 2px; 12 | } 13 | 14 | text { 15 | font: "Comfortaa.ttf"; 16 | font-size: 16px; 17 | color: !text; 18 | } 19 | 20 | window { 21 | align-x: center; 22 | } 23 | 24 | window-header { 25 | @apply window-outline; 26 | 27 | background-color: !window-header; 28 | radius: 2px; 29 | padding: 8px; 30 | } 31 | 32 | text .window-title { 33 | font: "Roboto.ttf"; 34 | font-size: 26px; 35 | color: #FFFFFF; 36 | align-x: right; 37 | text-shadow: rgb(15, 15, 15); 38 | } 39 | 40 | window-body { 41 | @apply window-outline; 42 | 43 | background-color: !window-body; 44 | radius: 2px; 45 | padding: 8px; 46 | } 47 | 48 | button { 49 | @apply outline; 50 | 51 | background-color: !bg; 52 | padding: 4px; 53 | spacing: 4px; 54 | icon: "click"; 55 | } 56 | 57 | button :hovered { 58 | background-color: !bg-hovered; 59 | } 60 | 61 | button :pressed { 62 | background-color: !bg-pressed; 63 | } 64 | 65 | icon .click { 66 | icon-path: "icons/click.svg"; 67 | color: #000000; 68 | size: 24px; 69 | } 70 | 71 | icon .click :hovered { 72 | color: rgb(20, 20, 20); 73 | } 74 | 75 | icon .click :pressed { 76 | color: rgb(40, 40, 40); 77 | } 78 | 79 | button-text { 80 | align-x: left; 81 | } 82 | 83 | checkbox { 84 | @apply outline; 85 | 86 | background-color: !bg; 87 | padding: 4px; 88 | radius: 6px; 89 | } 90 | 91 | checkbox :hovered { 92 | background-color: !bg-hovered; 93 | } 94 | 95 | checkbox :pressed { 96 | background-color: !bg-pressed; 97 | } 98 | 99 | checkbox-inner { 100 | color: rgb(0, 0, 0); 101 | size: 14px; 102 | icon-path: "icons/check.svg"; 103 | } 104 | 105 | checkbox-inner :hovered { 106 | color: rgb(20, 20, 20); 107 | } 108 | 109 | checkbox-inner :pressed { 110 | color: rgb(40, 40, 40); 111 | } 112 | 113 | horizontal-list { 114 | spacing: 8px; 115 | } 116 | 117 | text-box { 118 | @apply outline; 119 | 120 | background-color: rgb(240, 240, 240); 121 | color: !text; 122 | padding: 6px; 123 | icon: "write"; 124 | spacing: 4px; 125 | minimum-size: 220px 0px; 126 | } 127 | 128 | text-box-placeholder { 129 | color: rgb(145, 145, 145); 130 | } 131 | 132 | icon .write { 133 | icon-path: "icons/write.svg"; 134 | color: rgb(0, 0, 0); 135 | size: 16px; 136 | } 137 | 138 | selection { 139 | background-color: rgba(45, 125, 245, 100); 140 | radius: 2px; 141 | } 142 | 143 | // Separators 144 | 145 | horizontal-separator { 146 | size: 1px; 147 | background-color: rgb(210, 210, 210); 148 | } 149 | 150 | horizontal-separator-text { 151 | color: rgb(180, 180, 180); 152 | align-x: center; 153 | } 154 | 155 | scrollbar { 156 | padding: 2px; 157 | } 158 | 159 | scrollbar-handle { 160 | size.x: 6px; 161 | outline-size: 2px; 162 | outline-color: !window-header; 163 | radius: 3px; 164 | } 165 | 166 | scrollbar-handle :hovered { 167 | background-color: rgb(240, 240, 240); 168 | } 169 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeteorDevelopment/pulsar/462bb05a1a803e78e67b4142bda1b094748ed8ad/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /properties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "color", 4 | "type": "Color", 5 | "description": "The foreground color of widgets." 6 | }, 7 | { 8 | "name": "background-color", 9 | "type": "Color", 10 | "description": "The background color of widgets." 11 | }, 12 | 13 | { 14 | "name": "outline-size", 15 | "type": "float", 16 | "description": "Size of the background outline." 17 | }, 18 | { 19 | "name": "outline-color", 20 | "type": "Color", 21 | "description": "Color of the background outline." 22 | }, 23 | 24 | { 25 | "name": "spacing", 26 | "type": "Vector2", 27 | "description": "Size between neighbouring widgets in a container." 28 | }, 29 | { 30 | "name": "padding", 31 | "type": "Vector4", 32 | "description": "How far children widgets should be positioned from the edges." 33 | }, 34 | { 35 | "name": "radius", 36 | "type": "Vector4", 37 | "description": "How rounded the background and outline should be." 38 | }, 39 | { 40 | "name": "size", 41 | "type": "Vector2", 42 | "description": "Size of the widget." 43 | }, 44 | { 45 | "name": "minimum-size", 46 | "type": "Vector2", 47 | "description": "Minimum size of the widget." 48 | }, 49 | 50 | { 51 | "name": "align-x", 52 | "type": "Enum", 53 | "description": "How this widget should be aligned horizontally inside its cell." 54 | }, 55 | { 56 | "name": "align-y", 57 | "type": "Enum", 58 | "description": "How this widget should be aligned vertically inside its cell." 59 | }, 60 | 61 | { 62 | "name": "font-size", 63 | "type": "Number", 64 | "description": "How big the font for this text should be." 65 | }, 66 | { 67 | "name": "text-shadow", 68 | "type": "Color", 69 | "description": "Color of the text shadow." 70 | }, 71 | { 72 | "name": "text-shadow-offset", 73 | "type": "Vector2", 74 | "description": "How should be the text shadow positioned relative to the original text." 75 | }, 76 | { 77 | "name": "max-width", 78 | "type": "Number", 79 | "description": "Maximum width of text widgets before they start to wrap and span multiple lines." 80 | }, 81 | 82 | { 83 | "name": "list-direction", 84 | "type": "Enum", 85 | "description": "If this list widget should be reversed or not." 86 | }, 87 | 88 | { 89 | "name": "icon", 90 | "type": "Identifier", 91 | "description": "The icon tag used for this widget." 92 | }, 93 | { 94 | "name": "icon-path", 95 | "type": "String", 96 | "description": "The .svg file path relative to this file." 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /pts-intellij/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.intellij.tasks.BuildSearchableOptionsTask 2 | 3 | plugins { 4 | id("java") 5 | id("org.jetbrains.intellij") version "1.12.0" 6 | } 7 | 8 | group = "meteordevelopment" 9 | version = "0.1.0" 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation("org.antlr:antlr4-intellij-adaptor:0.1") 17 | 18 | implementation(project(":pts")) 19 | } 20 | 21 | intellij { 22 | version.set("2022.3") 23 | } 24 | 25 | tasks.withType { 26 | enabled = false 27 | } 28 | 29 | tasks.withType { 30 | sourceCompatibility = JavaVersion.VERSION_17.toString() 31 | targetCompatibility = JavaVersion.VERSION_17.toString() 32 | } 33 | 34 | tasks.withType { 35 | from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) 36 | } 37 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/PtsASTFactory.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts; 2 | 3 | import com.intellij.lang.DefaultASTFactoryImpl; 4 | import com.intellij.psi.impl.source.tree.LeafElement; 5 | import com.intellij.psi.tree.IElementType; 6 | import org.meteordev.pts.psi.PtsColor; 7 | import org.antlr.intellij.adaptor.lexer.TokenIElementType; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | public class PtsASTFactory extends DefaultASTFactoryImpl { 11 | @Override 12 | public @NotNull LeafElement createLeaf(@NotNull IElementType type, @NotNull CharSequence text) { 13 | if (type instanceof TokenIElementType tokenType && tokenType.getANTLRTokenType() == PtsLexer.HEX_COLOR) { 14 | return new PtsColor(type, text); 15 | } 16 | 17 | return super.createLeaf(type, text); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/PtsBraceMatcher.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts; 2 | 3 | import com.intellij.codeInsight.highlighting.PairedBraceMatcherAdapter; 4 | import com.intellij.lang.BracePair; 5 | import com.intellij.lang.PairedBraceMatcher; 6 | import com.intellij.psi.PsiFile; 7 | import com.intellij.psi.tree.IElementType; 8 | import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; 9 | import org.antlr.intellij.adaptor.lexer.TokenIElementType; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | import java.util.List; 14 | 15 | public class PtsBraceMatcher extends PairedBraceMatcherAdapter { 16 | public PtsBraceMatcher() { 17 | super( 18 | new PairedBraceMatcher() { 19 | @Override 20 | public BracePair @NotNull [] getPairs() { 21 | List types = PSIElementTypeFactory.getTokenIElementTypes(PtsLanguage.INSTANCE); 22 | 23 | return new BracePair[] { 24 | new BracePair(types.get(PtsLexer.OPENING_PAREN), types.get(PtsLexer.CLOSING_PAREN), false), 25 | new BracePair(types.get(PtsLexer.OPENING_BRACE), types.get(PtsLexer.CLOSING_BRACE), true), 26 | new BracePair(types.get(PtsLexer.OPENING_BRACKET), types.get(PtsLexer.CLOSING_BRACKET), false) 27 | }; 28 | } 29 | 30 | @Override 31 | public boolean isPairedBracesAllowedBeforeType(@NotNull IElementType lbraceType, @Nullable IElementType contextType) { 32 | return true; 33 | } 34 | 35 | @Override 36 | public int getCodeConstructStart(PsiFile file, int openingBraceOffset) { 37 | return openingBraceOffset; 38 | } 39 | }, 40 | PtsLanguage.INSTANCE 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/PtsCommenter.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts; 2 | 3 | import com.intellij.lang.Commenter; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public class PtsCommenter implements Commenter { 7 | @Override 8 | public @Nullable String getLineCommentPrefix() { 9 | return "//"; 10 | } 11 | 12 | @Override 13 | public @Nullable String getBlockCommentPrefix() { 14 | return "/*"; 15 | } 16 | 17 | @Override 18 | public @Nullable String getBlockCommentSuffix() { 19 | return "*/"; 20 | } 21 | 22 | @Override 23 | public @Nullable String getCommentedBlockCommentPrefix() { 24 | return null; 25 | } 26 | 27 | @Override 28 | public @Nullable String getCommentedBlockCommentSuffix() { 29 | return null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/PtsElementColorProvider.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts; 2 | 3 | import com.intellij.psi.PsiElement; 4 | import org.meteordev.pts.psi.PtsColor; 5 | import org.meteordev.pts.psi.PtsFunction; 6 | import org.antlr.intellij.adaptor.lexer.TokenIElementType; 7 | 8 | import java.awt.*; 9 | 10 | public class PtsElementColorProvider { 11 | public static PtsElementColorProvider INSTANCE = new PtsElementColorProvider(); 12 | 13 | @SuppressWarnings("UseJBColor") 14 | public Color getColorFrom(PsiElement element) { 15 | if (element instanceof PtsColor color) { 16 | // Hex color 17 | return color.getColor(); 18 | } 19 | else if (element.getNode().getElementType() instanceof TokenIElementType type && type.getANTLRTokenType() == PtsLexer.IDENTIFIER) { 20 | // Rgb / rgba color 21 | if (element.getParent() instanceof PtsFunction function) { 22 | String name = function.getNameElement().getText(); 23 | 24 | if (name.equals("rgb") || name.equals("rgba")) { 25 | int r = 255; 26 | int g = 255; 27 | int b = 255; 28 | int a = 255; 29 | 30 | int i = 0; 31 | for (PsiElement arg : function.argumentIterator()) { 32 | int number = (int) Double.parseDouble(arg.getText()); 33 | 34 | switch (i++) { 35 | case 0 -> r = number; 36 | case 1 -> g = number; 37 | case 2 -> b = number; 38 | case 3 -> a = number; 39 | } 40 | } 41 | 42 | return new Color(r, g, b, a); 43 | } 44 | } 45 | } 46 | 47 | return null; 48 | } 49 | 50 | public PsiElement setColorTo(PsiElement element, Color color) { 51 | if (element instanceof PtsColor) { 52 | // Hex color 53 | return element.replace(PtsElementFactory.createColor(element.getProject(), color)); 54 | } 55 | else { 56 | // Rgb / rgba color 57 | boolean rgba = color.getAlpha() < 255; 58 | String[] arguments = new String[rgba ? 4 : 3]; 59 | 60 | arguments[0] = Integer.toString(color.getRed()); 61 | arguments[1] = Integer.toString(color.getGreen()); 62 | arguments[2] = Integer.toString(color.getBlue()); 63 | if (rgba) arguments[3] = Integer.toString(color.getAlpha()); 64 | 65 | PtsFunction el = PtsElementFactory.createFunction(element.getProject(), rgba ? "rgba" : "rgb", arguments); 66 | return element.getParent().replace(el).getFirstChild(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/PtsElementFactory.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.psi.PsiFile; 5 | import com.intellij.psi.PsiFileFactory; 6 | import org.meteordev.pts.psi.PtsColor; 7 | import org.meteordev.pts.psi.PtsFunction; 8 | import org.meteordev.pts.psi.PtsProperty; 9 | import org.meteordev.pts.psi.PtsStyle; 10 | 11 | import java.awt.*; 12 | 13 | public class PtsElementFactory { 14 | private static PsiFile createFile(Project project, String text) { 15 | return PsiFileFactory.getInstance(project).createFileFromText("dummy.pts", PtsLanguage.INSTANCE, text); 16 | } 17 | 18 | public static PtsColor createColor(Project project, Color color) { 19 | StringBuilder sb = new StringBuilder("foo { bar: #"); 20 | 21 | sb.append(String.format("%02X", color.getRed())); 22 | sb.append(String.format("%02X", color.getGreen())); 23 | sb.append(String.format("%02X", color.getBlue())); 24 | if (color.getAlpha() < 255) sb.append(String.format("%02X", color.getAlpha())); 25 | 26 | sb.append("; }"); 27 | PsiFile file = createFile(project, sb.toString()); 28 | 29 | return (PtsColor) ((PtsProperty) ((PtsStyle) file.getFirstChild().getFirstChild().getFirstChild()).getChildFromEnd(1).getFirstChild()).getValueElement().getFirstChild().getFirstChild(); 30 | } 31 | 32 | public static PtsFunction createFunction(Project project, String name, String[] arguments) { 33 | StringBuilder sb = new StringBuilder("foo { bar: "); 34 | sb.append(name); 35 | sb.append('('); 36 | 37 | for (int i = 0; i < arguments.length; i++) { 38 | if (i > 0) sb.append(", "); 39 | sb.append(arguments[i]); 40 | } 41 | 42 | sb.append("); }"); 43 | PsiFile file = createFile(project, sb.toString()); 44 | 45 | return (PtsFunction) ((PtsProperty) ((PtsStyle) file.getFirstChild().getFirstChild().getFirstChild()).getChildFromEnd(1).getFirstChild()).getValueElement().getFirstChild(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/PtsFileType.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts; 2 | 3 | import com.intellij.openapi.fileTypes.LanguageFileType; 4 | import com.intellij.openapi.util.NlsContexts; 5 | import com.intellij.openapi.util.NlsSafe; 6 | import org.jetbrains.annotations.NonNls; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import javax.swing.*; 10 | 11 | public class PtsFileType extends LanguageFileType { 12 | public static final PtsFileType INSTANCE = new PtsFileType(); 13 | 14 | public PtsFileType() { 15 | super(PtsLanguage.INSTANCE); 16 | } 17 | 18 | @Override 19 | public @NonNls @NotNull String getName() { 20 | return "PTS File"; 21 | } 22 | 23 | @Override 24 | public @NlsContexts.Label @NotNull String getDescription() { 25 | return "Pulsar theme styles file"; 26 | } 27 | 28 | @Override 29 | public @NlsSafe @NotNull String getDefaultExtension() { 30 | return "pts"; 31 | } 32 | 33 | @Override 34 | public Icon getIcon() { 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/PtsFoldingBuilder.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.lang.folding.FoldingBuilderEx; 5 | import com.intellij.lang.folding.FoldingDescriptor; 6 | import com.intellij.openapi.editor.Document; 7 | import com.intellij.openapi.util.TextRange; 8 | import com.intellij.psi.PsiElement; 9 | import com.intellij.psi.util.PsiTreeUtil; 10 | import org.meteordev.pts.psi.PtsStyle; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | public class PtsFoldingBuilder extends FoldingBuilderEx { 18 | @Override 19 | public FoldingDescriptor @NotNull [] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, boolean quick) { 20 | List descriptors = new ArrayList<>(); 21 | 22 | for (PsiElement element : PsiTreeUtil.findChildrenOfType(root, PtsStyle.class)) { 23 | descriptors.add(new FoldingDescriptor( 24 | element, 25 | new TextRange( 26 | element.getTextRange().getStartOffset(), 27 | element.getTextRange().getEndOffset() 28 | ) 29 | )); 30 | } 31 | 32 | return descriptors.toArray(new FoldingDescriptor[0]); 33 | } 34 | 35 | @Override 36 | public boolean isCollapsedByDefault(@NotNull ASTNode node) { 37 | return false; 38 | } 39 | 40 | @Override 41 | public @Nullable String getPlaceholderText(@NotNull ASTNode node) { 42 | return ((PtsStyle) node.getPsi()).getSelector(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/PtsLanguage.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts; 2 | 3 | import com.intellij.lang.Language; 4 | 5 | public class PtsLanguage extends Language { 6 | public static final PtsLanguage INSTANCE = new PtsLanguage(); 7 | 8 | public PtsLanguage() { 9 | super("PTS"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/PtsParserDefinition.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.lang.ParserDefinition; 5 | import com.intellij.lang.PsiParser; 6 | import com.intellij.lexer.Lexer; 7 | import com.intellij.openapi.project.Project; 8 | import com.intellij.psi.FileViewProvider; 9 | import com.intellij.psi.PsiElement; 10 | import com.intellij.psi.PsiFile; 11 | import com.intellij.psi.tree.IElementType; 12 | import com.intellij.psi.tree.IFileElementType; 13 | import com.intellij.psi.tree.TokenSet; 14 | import org.antlr.intellij.adaptor.lexer.ANTLRLexerAdaptor; 15 | import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; 16 | import org.antlr.intellij.adaptor.lexer.RuleIElementType; 17 | import org.antlr.intellij.adaptor.lexer.TokenIElementType; 18 | import org.antlr.intellij.adaptor.parser.ANTLRParserAdaptor; 19 | import org.antlr.intellij.adaptor.psi.ANTLRPsiNode; 20 | import org.antlr.v4.runtime.Parser; 21 | import org.antlr.v4.runtime.tree.ParseTree; 22 | import org.jetbrains.annotations.NotNull; 23 | import org.meteordev.pts.psi.*; 24 | 25 | public class PtsParserDefinition implements ParserDefinition { 26 | public static final IFileElementType FILE = new IFileElementType(PtsLanguage.INSTANCE); 27 | 28 | static { 29 | PSIElementTypeFactory.defineLanguageIElementTypes(PtsLanguage.INSTANCE, PtsParser.tokenNames, PtsParser.ruleNames); 30 | } 31 | 32 | public static final TokenSet COMMENTS = PSIElementTypeFactory.createTokenSet(PtsLanguage.INSTANCE, PtsLexer.COMMENT, PtsLexer.LINE_COMMENT); 33 | public static final TokenSet WHITESPACE = PSIElementTypeFactory.createTokenSet(PtsLanguage.INSTANCE, PtsLexer.WS); 34 | public static final TokenSet STRING = PSIElementTypeFactory.createTokenSet(PtsLanguage.INSTANCE, PtsLexer.STRING); 35 | 36 | @Override 37 | public @NotNull Lexer createLexer(Project project) { 38 | PtsLexer lexer = new PtsLexer(null); 39 | return new ANTLRLexerAdaptor(PtsLanguage.INSTANCE, lexer); 40 | } 41 | 42 | @Override 43 | public @NotNull PsiParser createParser(Project project) { 44 | PtsParser parser = new PtsParser(null); 45 | return new ANTLRParserAdaptor(PtsLanguage.INSTANCE, parser) { 46 | @Override 47 | protected ParseTree parse(Parser parser, IElementType root) { 48 | return ((PtsParser) parser).pts(); 49 | } 50 | }; 51 | } 52 | 53 | @Override 54 | public @NotNull IFileElementType getFileNodeType() { 55 | return FILE; 56 | } 57 | 58 | @Override 59 | public @NotNull TokenSet getCommentTokens() { 60 | return COMMENTS; 61 | } 62 | 63 | @Override 64 | public @NotNull TokenSet getWhitespaceTokens() { 65 | return WHITESPACE; 66 | } 67 | 68 | @Override 69 | public @NotNull TokenSet getStringLiteralElements() { 70 | return STRING; 71 | } 72 | 73 | @Override 74 | public @NotNull PsiElement createElement(ASTNode node) { 75 | IElementType type = node.getElementType(); 76 | 77 | if (type instanceof TokenIElementType) return new ANTLRPsiNode(node); 78 | if (!(type instanceof RuleIElementType rule)) return new ANTLRPsiNode(node); 79 | 80 | return switch (rule.getRuleIndex()) { 81 | case PtsParser.RULE_atStatement -> new PtsAtStatement(node, false); 82 | case PtsParser.RULE_atVar -> new PtsAtVar(node); 83 | case PtsParser.RULE_style -> new PtsStyle(node); 84 | case PtsParser.RULE_apply -> new PtsAtStatement(node, true); 85 | case PtsParser.RULE_property -> new PtsProperty(node); 86 | case PtsParser.RULE_function -> new PtsFunction(node); 87 | default -> new PtsPsiNode(node); 88 | }; 89 | } 90 | 91 | @Override 92 | public @NotNull PsiFile createFile(@NotNull FileViewProvider viewProvider) { 93 | return new PtsPSIFileRoot(viewProvider); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/PtsQuoteHandler.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts; 2 | 3 | import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler; 4 | import com.intellij.psi.tree.TokenSet; 5 | import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; 6 | 7 | public class PtsQuoteHandler extends SimpleTokenSetQuoteHandler { 8 | public static final TokenSet QUOTE = PSIElementTypeFactory.createTokenSet(PtsLanguage.INSTANCE, PtsLexer.QUOTE); 9 | 10 | public PtsQuoteHandler() { 11 | super(QUOTE); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/completion/Completions.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.completion; 2 | 3 | import com.intellij.codeInsight.completion.InsertionContext; 4 | import com.intellij.codeInsight.lookup.LookupElement; 5 | import com.intellij.codeInsight.lookup.LookupElementBuilder; 6 | import com.intellij.openapi.editor.CaretModel; 7 | import com.intellij.util.PlatformIcons; 8 | import org.meteordev.pts.properties.Properties; 9 | import org.meteordev.pts.properties.Property; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class Completions { 16 | public static final List AT_STATEMENTS = Arrays.asList( 17 | createAt("title", AtAfter.String), 18 | createAt("authors", AtAfter.StringArray), 19 | createAt("include", AtAfter.String), 20 | createAt("var", AtAfter.Identifier), 21 | createAt("mixin", AtAfter.Identifier) 22 | ); 23 | 24 | public static final List PROPERTIES = new ArrayList<>(); 25 | 26 | static { 27 | for (Property property : Properties.getAll()) PROPERTIES.add(createProperty(property)); 28 | } 29 | 30 | private static LookupElement createAt(String name, AtAfter after) { 31 | return LookupElementBuilder 32 | .create("@" + name) 33 | .withIcon(PlatformIcons.FIELD_ICON) 34 | .withTypeText(after.toString()) 35 | .withInsertHandler((context, item) -> { 36 | switch (after) { 37 | case String -> insert(context, " \"\";", 2); 38 | case StringArray -> insert(context, " [ \"\" ];", 4); 39 | case Identifier -> insert(context, " ", 1); 40 | } 41 | }); 42 | } 43 | 44 | private static LookupElement createProperty(Property property) { 45 | return LookupElementBuilder 46 | .create(property.name()) 47 | .withIcon(PlatformIcons.PROPERTY_ICON) 48 | .withTypeText(space(property.type().name)) 49 | .withInsertHandler((context, item) -> insert(context, ": ;", 2)); 50 | } 51 | 52 | private static String space(String string) { 53 | return string.replaceAll("(\\p{Ll})(\\p{Lu})","$1 $2"); 54 | } 55 | 56 | private static void insert(InsertionContext context, String string, int caretOffset) { 57 | CaretModel caret = context.getEditor().getCaretModel(); 58 | 59 | context.getDocument().insertString(caret.getOffset(), string); 60 | caret.moveToOffset(caret.getOffset() + caretOffset); 61 | } 62 | 63 | private enum AtAfter { 64 | String, 65 | StringArray, 66 | Identifier; 67 | 68 | @Override 69 | public java.lang.String toString() { 70 | return this == StringArray ? "String[]" : super.toString(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/completion/PtsCharFilter.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.completion; 2 | 3 | import com.intellij.codeInsight.lookup.CharFilter; 4 | import com.intellij.codeInsight.lookup.Lookup; 5 | import org.meteordev.pts.PtsLanguage; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | public class PtsCharFilter extends CharFilter { 9 | @Override 10 | public @Nullable Result acceptChar(char c, int prefixLength, Lookup lookup) { 11 | if (lookup.getPsiFile().getLanguage() != PtsLanguage.INSTANCE) return null; 12 | 13 | if ( 14 | Character.isLetter(c) 15 | || Character.isDigit(c) 16 | || c == '_' 17 | || c == '-' 18 | || c == '@' 19 | ) return Result.ADD_TO_PREFIX; 20 | 21 | if (c == '.' || c == ':') return Result.SELECT_ITEM_AND_FINISH_LOOKUP; 22 | 23 | return Result.HIDE_LOOKUP; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/completion/PtsCompletionContributor.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.completion; 2 | 3 | import com.intellij.codeInsight.completion.*; 4 | import com.intellij.codeInsight.lookup.LookupElement; 5 | import com.intellij.patterns.PlatformPatterns; 6 | import com.intellij.psi.PsiElement; 7 | import com.intellij.psi.PsiErrorElement; 8 | import com.intellij.psi.PsiFile; 9 | import com.intellij.util.ProcessingContext; 10 | import org.meteordev.pts.psi.PtsProperty; 11 | import org.meteordev.pts.psi.PtsStyle; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.util.List; 15 | 16 | public class PtsCompletionContributor extends CompletionContributor { 17 | public PtsCompletionContributor() { 18 | extend( 19 | CompletionType.BASIC, 20 | PlatformPatterns.psiElement().withParent(PtsStyle.class), 21 | new MyCompletionProvider<>(Completions.AT_STATEMENTS) 22 | ); 23 | 24 | extend( 25 | CompletionType.BASIC, 26 | PlatformPatterns.psiElement().withParent(PlatformPatterns.psiElement(PsiErrorElement.class).withParent(PtsProperty.class)), 27 | new MyCompletionProvider<>(Completions.PROPERTIES) 28 | ); 29 | } 30 | 31 | @Override 32 | public void beforeCompletion(@NotNull CompletionInitializationContext context) { 33 | PsiFile file = context.getFile(); 34 | PsiElement element = file.findElementAt(context.getStartOffset()); 35 | //System.out.println(element); 36 | } 37 | 38 | private static class MyCompletionProvider extends CompletionProvider { 39 | private final List elements; 40 | 41 | public MyCompletionProvider(List elements) { 42 | this.elements = elements; 43 | } 44 | 45 | @Override 46 | protected void addCompletions(@NotNull T parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet result) { 47 | result.addAllElements(elements); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/highlight/PtsSyntaxHighlighter.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.highlight; 2 | 3 | import com.intellij.lexer.Lexer; 4 | import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; 5 | import com.intellij.openapi.editor.HighlighterColors; 6 | import com.intellij.openapi.editor.XmlHighlighterColors; 7 | import com.intellij.openapi.editor.colors.TextAttributesKey; 8 | import com.intellij.openapi.fileTypes.SyntaxHighlighterBase; 9 | import com.intellij.psi.tree.IElementType; 10 | import org.meteordev.pts.PtsLanguage; 11 | import org.meteordev.pts.PtsLexer; 12 | import org.antlr.intellij.adaptor.lexer.ANTLRLexerAdaptor; 13 | import org.antlr.intellij.adaptor.lexer.TokenIElementType; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | import static com.intellij.openapi.editor.colors.TextAttributesKey.createTextAttributesKey; 17 | 18 | public class PtsSyntaxHighlighter extends SyntaxHighlighterBase { 19 | private static final TextAttributesKey[] EMPTY = new TextAttributesKey[0]; 20 | 21 | public static final TextAttributesKey NUMBER = createTextAttributesKey("PTS_NUMBER", DefaultLanguageHighlighterColors.NUMBER); 22 | public static final TextAttributesKey COLOR = createTextAttributesKey("PTS_COLOR", DefaultLanguageHighlighterColors.NUMBER); 23 | public static final TextAttributesKey STRING = createTextAttributesKey("PTS_STRING", DefaultLanguageHighlighterColors.STRING); 24 | public static final TextAttributesKey IDENTIFIER = createTextAttributesKey("PTS_IDENTIFIER", DefaultLanguageHighlighterColors.IDENTIFIER); 25 | 26 | public static final TextAttributesKey BRACES = createTextAttributesKey("PTS_BRACES", DefaultLanguageHighlighterColors.BRACES); 27 | public static final TextAttributesKey BRACKETS = createTextAttributesKey("PTS_BRACKETS", DefaultLanguageHighlighterColors.BRACKETS); 28 | public static final TextAttributesKey COMMA = createTextAttributesKey("PTS_COMMA", DefaultLanguageHighlighterColors.COMMA); 29 | public static final TextAttributesKey SEMICOLON = createTextAttributesKey("PTS_SEMICOLON", DefaultLanguageHighlighterColors.SEMICOLON); 30 | public static final TextAttributesKey VARIABLE = createTextAttributesKey("PTS_VARIABLE", XmlHighlighterColors.HTML_TAG_NAME); 31 | 32 | public static final TextAttributesKey LINE_COMMENT = createTextAttributesKey("PTS_LINE_COMMENT", DefaultLanguageHighlighterColors.LINE_COMMENT); 33 | public static final TextAttributesKey BLOCK_COMMENT = createTextAttributesKey("PTS_BLOCK_COMMENT", DefaultLanguageHighlighterColors.BLOCK_COMMENT); 34 | 35 | public static final TextAttributesKey UNKNOWN = createTextAttributesKey("PTS_BAD_CHARACTER", HighlighterColors.BAD_CHARACTER); 36 | 37 | @Override 38 | public @NotNull Lexer getHighlightingLexer() { 39 | PtsLexer lexer = new PtsLexer(null); 40 | return new ANTLRLexerAdaptor(PtsLanguage.INSTANCE, lexer); 41 | } 42 | 43 | @Override 44 | public TextAttributesKey @NotNull [] getTokenHighlights(IElementType tokenType) { 45 | if (!(tokenType instanceof TokenIElementType myType)) return EMPTY; 46 | 47 | int type = myType.getANTLRTokenType(); 48 | TextAttributesKey key; 49 | 50 | switch (type) { 51 | case PtsLexer.NUMBER, PtsLexer.PX -> key = NUMBER; 52 | case PtsLexer.HEX_COLOR -> key = COLOR; 53 | case PtsLexer.STRING -> key = STRING; 54 | case PtsLexer.IDENTIFIER -> key = IDENTIFIER; 55 | 56 | case PtsLexer.OPENING_BRACE, PtsLexer.CLOSING_BRACE -> key = BRACES; 57 | case PtsLexer.OPENING_BRACKET, PtsLexer.CLOSING_BRACKET -> key = BRACKETS; 58 | case PtsLexer.COMMA -> key = COMMA; 59 | case PtsLexer.SEMICOLON -> key = SEMICOLON; 60 | case PtsLexer.BANG -> key = VARIABLE; 61 | 62 | case PtsLexer.COMMENT -> key = BLOCK_COMMENT; 63 | case PtsLexer.LINE_COMMENT -> key = LINE_COMMENT; 64 | 65 | case PtsLexer.UNKNOWN -> key = UNKNOWN; 66 | 67 | default -> { return EMPTY; } 68 | } 69 | 70 | return new TextAttributesKey[] { key }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/highlight/PtsSyntaxHighlighterFactory.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.highlight; 2 | 3 | import com.intellij.openapi.fileTypes.SyntaxHighlighter; 4 | import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | public class PtsSyntaxHighlighterFactory extends SyntaxHighlighterFactory { 11 | @Override 12 | public @NotNull SyntaxHighlighter getSyntaxHighlighter(@Nullable Project project, @Nullable VirtualFile virtualFile) { 13 | return new PtsSyntaxHighlighter(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/psi/PtsAtStatement.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.psi; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.psi.PsiElement; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public class PtsAtStatement extends PtsPsiNode { 8 | public final boolean isApply; 9 | 10 | public PtsAtStatement(@NotNull ASTNode node, boolean isApply) { 11 | super(node); 12 | 13 | this.isApply = isApply; 14 | } 15 | 16 | public PsiElement getKeywordElement() { 17 | return isApply ? getChildFromStart(0) : getChildFromStart(0).getFirstChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/psi/PtsAtVar.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.psi; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.psi.PsiElement; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public class PtsAtVar extends PtsPsiNode { 8 | public PtsAtVar(@NotNull ASTNode node) { 9 | super(node); 10 | } 11 | 12 | public PsiElement getNameElement() { 13 | return getChildFromStart(1); 14 | } 15 | 16 | public PsiElement getTypeElement() { 17 | return getChildFromStart(3); 18 | } 19 | 20 | public PsiElement getValueElement() { 21 | return getChildFromEnd(1); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/psi/PtsColor.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.psi; 2 | 3 | import com.intellij.psi.impl.source.tree.LeafPsiElement; 4 | import com.intellij.psi.tree.IElementType; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.awt.*; 8 | 9 | public class PtsColor extends LeafPsiElement { 10 | public PtsColor(@NotNull IElementType type, @NotNull CharSequence text) { 11 | super(type, text); 12 | } 13 | 14 | @SuppressWarnings("UseJBColor") 15 | public Color getColor() { 16 | String text = getText(); 17 | 18 | return new Color( 19 | Integer.parseInt(text.substring(1, 3), 16), 20 | Integer.parseInt(text.substring(3, 5), 16), 21 | Integer.parseInt(text.substring(5, 7), 16), 22 | text.length() > 7 ? Integer.parseInt(text.substring(7, 9), 16) : 255 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/psi/PtsFunction.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.psi; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.psi.PsiElement; 5 | import org.meteordev.pts.PtsLexer; 6 | import org.antlr.intellij.adaptor.lexer.TokenIElementType; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.util.Iterator; 10 | 11 | public class PtsFunction extends PtsPsiNode { 12 | public PtsFunction(@NotNull ASTNode node) { 13 | super(node); 14 | } 15 | 16 | public PsiElement getNameElement() { 17 | return getChildFromStart(0); 18 | } 19 | 20 | public Iterable argumentIterator() { 21 | return () -> new ArgumentIterator(childIterator().iterator()); 22 | } 23 | 24 | private static class ArgumentIterator implements Iterator { 25 | private final Iterator childIt; 26 | private PsiElement next; 27 | 28 | public ArgumentIterator(Iterator childIt) { 29 | this.childIt = childIt; 30 | getNext(); 31 | } 32 | 33 | private void getNext() { 34 | while (true) { 35 | if (!childIt.hasNext()) { 36 | next = null; 37 | break; 38 | } 39 | 40 | next = childIt.next(); 41 | 42 | if (next.getNode().getElementType() instanceof TokenIElementType type) { 43 | int token = type.getANTLRTokenType(); 44 | if (token != PtsLexer.IDENTIFIER && token != PtsLexer.OPENING_PAREN && token != PtsLexer.COMMA && token != PtsLexer.CLOSING_PAREN) break; 45 | } 46 | else break; 47 | } 48 | } 49 | 50 | @Override 51 | public boolean hasNext() { 52 | return next != null; 53 | } 54 | 55 | @Override 56 | public PsiElement next() { 57 | PsiElement arg = next; 58 | getNext(); 59 | return arg; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/psi/PtsPSIFileRoot.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.psi; 2 | 3 | import com.intellij.extapi.psi.PsiFileBase; 4 | import com.intellij.openapi.fileTypes.FileType; 5 | import com.intellij.psi.FileViewProvider; 6 | import org.meteordev.pts.PtsFileType; 7 | import org.meteordev.pts.PtsLanguage; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | public class PtsPSIFileRoot extends PsiFileBase { 11 | public PtsPSIFileRoot(@NotNull FileViewProvider viewProvider) { 12 | super(viewProvider, PtsLanguage.INSTANCE); 13 | } 14 | 15 | @Override 16 | public @NotNull FileType getFileType() { 17 | return PtsFileType.INSTANCE; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return "PTS File"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/psi/PtsProperty.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.psi; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.psi.PsiElement; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public class PtsProperty extends PtsPsiNode { 8 | public PtsProperty(@NotNull ASTNode node) { 9 | super(node); 10 | } 11 | 12 | public PsiElement getNameElement() { 13 | return getChildFromStart(0); 14 | } 15 | 16 | public PsiElement getValueElement() { 17 | return getChildFromEnd(1); 18 | } 19 | 20 | @Override 21 | public String getName() { 22 | return getNameElement().getText(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/psi/PtsPsiNode.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.psi; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.psi.PsiComment; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiWhiteSpace; 7 | import org.antlr.intellij.adaptor.psi.ANTLRPsiNode; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.Iterator; 11 | 12 | public class PtsPsiNode extends ANTLRPsiNode { 13 | public PtsPsiNode(@NotNull ASTNode node) { 14 | super(node); 15 | } 16 | 17 | public PsiElement getChildFromStart(int i) { 18 | PsiElement child = getFirstValid(getFirstChild(), true); 19 | if (child == null) return null; 20 | 21 | for (int j = 0; j < i; j++) { 22 | child = getFirstValid(child.getNextSibling(), true); 23 | if (child == null) return null; 24 | } 25 | 26 | return child; 27 | } 28 | 29 | public PsiElement getChildFromEnd(int i) { 30 | PsiElement child = getFirstValid(getLastChild(), false); 31 | if (child == null) return null; 32 | 33 | for (int j = 0; j < i; j++) { 34 | child = getFirstValid(child.getPrevSibling(), false); 35 | if (child == null) return null; 36 | } 37 | 38 | return child; 39 | } 40 | 41 | public Iterable childIterator() { 42 | return () -> new ChildIterator(getFirstChild()); 43 | } 44 | 45 | private static PsiElement getFirstValid(PsiElement element, boolean next) { 46 | if (element == null) return null; 47 | 48 | while (element instanceof PsiWhiteSpace || element instanceof PsiComment) { 49 | if (next) element = element.getNextSibling(); 50 | else element = element.getPrevSibling(); 51 | 52 | if (element == null) return null; 53 | } 54 | 55 | return element; 56 | } 57 | 58 | private static class ChildIterator implements Iterator { 59 | private PsiElement next; 60 | 61 | private ChildIterator(PsiElement first) { 62 | this.next = getFirstValid(first, true); 63 | } 64 | 65 | @Override 66 | public boolean hasNext() { 67 | return next != null; 68 | } 69 | 70 | @Override 71 | public PsiElement next() { 72 | PsiElement child = next; 73 | next = getFirstValid(next.getNextSibling(), true); 74 | return child; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/psi/PtsStyle.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.psi; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.psi.PsiComment; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiWhiteSpace; 7 | import com.intellij.psi.tree.IElementType; 8 | import org.meteordev.pts.PtsLexer; 9 | import org.antlr.intellij.adaptor.lexer.TokenIElementType; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.util.function.Consumer; 13 | 14 | public class PtsStyle extends PtsPsiNode { 15 | public PtsStyle(@NotNull ASTNode node) { 16 | super(node); 17 | } 18 | 19 | public void forEachChildInSelector(Consumer callback, boolean includeWhiteSpace) { 20 | PsiElement child = getFirstChild(); 21 | 22 | while (child != null) { 23 | IElementType type = child.getNode().getElementType(); 24 | if (type instanceof TokenIElementType tokenType && tokenType.getANTLRTokenType() == PtsLexer.OPENING_BRACE) break; 25 | 26 | if ((includeWhiteSpace || !(child instanceof PsiWhiteSpace)) && !(child instanceof PsiComment)) callback.accept(child); 27 | 28 | child = child.getNextSibling(); 29 | } 30 | } 31 | 32 | public String getSelector() { 33 | StringBuilder sb = new StringBuilder(); 34 | 35 | forEachChildInSelector(child -> sb.append(child.getText()), true); 36 | 37 | return sb.toString().trim(); 38 | } 39 | 40 | @Override 41 | public String getName() { 42 | return getSelector(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/structure/PtsStructureAwareNavBar.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.structure; 2 | 3 | import com.intellij.ide.navigationToolbar.StructureAwareNavBarModelExtension; 4 | import com.intellij.lang.Language; 5 | import com.intellij.util.PlatformIcons; 6 | import org.meteordev.pts.PtsLanguage; 7 | import org.meteordev.pts.psi.PtsProperty; 8 | import org.meteordev.pts.psi.PtsPsiNode; 9 | import org.meteordev.pts.psi.PtsStyle; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | import javax.swing.*; 14 | 15 | public class PtsStructureAwareNavBar extends StructureAwareNavBarModelExtension { 16 | @NotNull 17 | @Override 18 | protected Language getLanguage() { 19 | return PtsLanguage.INSTANCE; 20 | } 21 | 22 | @Override 23 | public @Nullable String getPresentableText(Object object) { 24 | if (object instanceof PtsPsiNode node) return node.getName(); 25 | 26 | return null; 27 | } 28 | 29 | @Override 30 | public @Nullable Icon getIcon(Object object) { 31 | if (object instanceof PtsStyle) return PlatformIcons.PROPERTIES_ICON; 32 | if (object instanceof PtsProperty) return PlatformIcons.PROPERTY_ICON; 33 | 34 | return null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/structure/PtsStructureViewElement.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.structure; 2 | 3 | import com.intellij.ide.projectView.PresentationData; 4 | import com.intellij.ide.structureView.StructureViewTreeElement; 5 | import com.intellij.ide.util.treeView.smartTree.SortableTreeElement; 6 | import com.intellij.ide.util.treeView.smartTree.TreeElement; 7 | import com.intellij.navigation.ItemPresentation; 8 | import com.intellij.psi.NavigatablePsiElement; 9 | import com.intellij.psi.util.PsiTreeUtil; 10 | import com.intellij.util.PlatformIcons; 11 | import org.meteordev.pts.psi.PtsPSIFileRoot; 12 | import org.meteordev.pts.psi.PtsProperty; 13 | import org.meteordev.pts.psi.PtsStyle; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | import java.util.Collection; 17 | 18 | public class PtsStructureViewElement implements StructureViewTreeElement, SortableTreeElement { 19 | private final NavigatablePsiElement element; 20 | 21 | public PtsStructureViewElement(NavigatablePsiElement element) { 22 | this.element = element; 23 | } 24 | 25 | @Override 26 | public Object getValue() { 27 | return element; 28 | } 29 | 30 | @Override 31 | public @NotNull String getAlphaSortKey() { 32 | String name = element.getName(); 33 | return name != null ? name : ""; 34 | } 35 | 36 | @Override 37 | public @NotNull ItemPresentation getPresentation() { 38 | if (element instanceof PtsStyle) return new PresentationData(element.getName(), null, PlatformIcons.PROPERTIES_ICON, null); 39 | if (element instanceof PtsProperty) return new PresentationData(element.getName(), null, PlatformIcons.PROPERTY_ICON, null); 40 | 41 | ItemPresentation presentation = element.getPresentation(); 42 | return presentation != null ? presentation : new PresentationData(); 43 | } 44 | 45 | @Override 46 | public TreeElement @NotNull [] getChildren() { 47 | if (element instanceof PtsPSIFileRoot) { 48 | Collection styles = PsiTreeUtil.findChildrenOfType(element, PtsStyle.class); 49 | TreeElement[] elements = new TreeElement[styles.size()]; 50 | 51 | int i = 0; 52 | for (PtsStyle style : styles) { 53 | elements[i++] = new PtsStructureViewElement(style); 54 | } 55 | 56 | return elements; 57 | } 58 | else if (element instanceof PtsStyle) { 59 | Collection properties = PsiTreeUtil.findChildrenOfType(element, PtsProperty.class); 60 | TreeElement[] elements = new TreeElement[properties.size()]; 61 | 62 | int i = 0; 63 | for (PtsProperty property : properties) { 64 | elements[i++] = new PtsStructureViewElement(property); 65 | } 66 | 67 | return elements; 68 | } 69 | 70 | return EMPTY_ARRAY; 71 | } 72 | 73 | @Override 74 | public void navigate(boolean requestFocus) { 75 | element.navigate(requestFocus); 76 | } 77 | 78 | @Override 79 | public boolean canNavigate() { 80 | return element.canNavigate(); 81 | } 82 | 83 | @Override 84 | public boolean canNavigateToSource() { 85 | return element.canNavigateToSource(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/structure/PtsStructureViewFactory.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.structure; 2 | 3 | import com.intellij.ide.structureView.StructureViewBuilder; 4 | import com.intellij.ide.structureView.StructureViewModel; 5 | import com.intellij.ide.structureView.TreeBasedStructureViewBuilder; 6 | import com.intellij.lang.PsiStructureViewFactory; 7 | import com.intellij.openapi.editor.Editor; 8 | import com.intellij.psi.PsiFile; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | public class PtsStructureViewFactory implements PsiStructureViewFactory { 13 | @Override 14 | public @Nullable StructureViewBuilder getStructureViewBuilder(@NotNull PsiFile psiFile) { 15 | return new TreeBasedStructureViewBuilder() { 16 | @Override 17 | public @NotNull StructureViewModel createStructureViewModel(@Nullable Editor editor) { 18 | return new PtsStructureViewModel(editor, psiFile); 19 | } 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pts-intellij/src/main/java/org/meteordev/pts/structure/PtsStructureViewModel.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.structure; 2 | 3 | import com.intellij.ide.structureView.StructureViewModel; 4 | import com.intellij.ide.structureView.StructureViewModelBase; 5 | import com.intellij.ide.structureView.StructureViewTreeElement; 6 | import com.intellij.ide.util.treeView.smartTree.Sorter; 7 | import com.intellij.openapi.editor.Editor; 8 | import com.intellij.psi.PsiFile; 9 | import org.meteordev.pts.psi.PtsProperty; 10 | import org.meteordev.pts.psi.PtsStyle; 11 | 12 | public class PtsStructureViewModel extends StructureViewModelBase implements StructureViewModel.ElementInfoProvider { 13 | public PtsStructureViewModel(Editor editor, PsiFile file) { 14 | super(file, editor, new PtsStructureViewElement(file)); 15 | 16 | withSorters(Sorter.ALPHA_SORTER); 17 | withSuitableClasses(PtsStyle.class, PtsProperty.class); 18 | } 19 | 20 | @Override 21 | public boolean isAlwaysShowsPlus(StructureViewTreeElement element) { 22 | return false; 23 | } 24 | 25 | @Override 26 | public boolean isAlwaysLeaf(StructureViewTreeElement element) { 27 | return element.getValue() instanceof PtsProperty; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pts-intellij/src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | meteordevelopment.pts 3 | PTS 4 | Meteor Development 5 | 6 | com.intellij.modules.platform 7 | 8 | 9 | 15 | 16 | 19 | 20 | 23 | 24 | 27 | 28 | 30 | 31 | 34 | 35 | 38 | 39 | 42 | 43 | 46 | 47 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | 59 | 62 | 63 | -------------------------------------------------------------------------------- /pts-vscode/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /pts-vscode/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /pts-vscode/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /pts-vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /pts-vscode/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /pts-vscode/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /pts-vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /pts-vscode/.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /pts-vscode/README.md: -------------------------------------------------------------------------------- 1 | # Pulsar Theme Styles (pts) 2 | Language for theme definition in Pulsar. -------------------------------------------------------------------------------- /pts-vscode/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": [ "/*", "*/" ] 5 | }, 6 | 7 | "brackets": [ [ "{", "}" ] ], 8 | "autoClosingPairs": [ { 9 | "open": "{", 10 | "close": "}", 11 | "notIn": [ "comment", "string" ] 12 | } ], 13 | 14 | "folding": { 15 | "markers": { 16 | "start": "{", 17 | "end": "}" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /pts-vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pts", 3 | "displayName": "Pulsar Theme Styles", 4 | "description": "Language for theme definition in Pulsar.", 5 | "publisher": "MineGame159", 6 | "version": "0.0.2", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/MeteorDevelopment/pulsar.git" 10 | }, 11 | "engines": { 12 | "vscode": "^1.62.0" 13 | }, 14 | "categories": [ 15 | "Programming Languages" 16 | ], 17 | "activationEvents": [ 18 | "onLanguage:pts" 19 | ], 20 | "main": "./out/extension.js", 21 | "contributes": { 22 | "languages": [ 23 | { 24 | "id": "pts", 25 | "aliases": [ 26 | "pts", 27 | "PTS" 28 | ], 29 | "extensions": [ 30 | ".pts" 31 | ], 32 | "configuration": "./language-configuration.json" 33 | } 34 | ], 35 | "grammars": [ 36 | { 37 | "language": "pts", 38 | "scopeName": "source.pts", 39 | "path": "./syntaxes/pts.tmLanguage.json" 40 | } 41 | ] 42 | }, 43 | "scripts": { 44 | "vscode:prepublish": "yarn run compile", 45 | "compile": "tsc -p ./", 46 | "watch": "tsc -watch -p ./", 47 | "pretest": "yarn run compile && yarn run lint", 48 | "lint": "eslint src --ext ts", 49 | "test": "node ./out/test/runTest.js" 50 | }, 51 | "devDependencies": { 52 | "@types/glob": "^7.1.4", 53 | "@types/mocha": "^9.0.0", 54 | "@types/node": "14.x", 55 | "@types/vscode": "^1.62.0", 56 | "@typescript-eslint/eslint-plugin": "^4.31.1", 57 | "@typescript-eslint/parser": "^4.31.1", 58 | "@vscode/test-electron": "^1.6.2", 59 | "eslint": "^7.32.0", 60 | "glob": "^7.1.7", 61 | "mocha": "^9.1.1", 62 | "typescript": "^4.4.3" 63 | }, 64 | "dependencies": { 65 | "cross-fetch": "^3.1.4" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pts-vscode/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import fetch from "cross-fetch" 3 | 4 | interface Property { 5 | name: string 6 | type: string 7 | description: string 8 | } 9 | 10 | export function activate(context: vscode.ExtensionContext) { 11 | let completionItems: vscode.CompletionItem[] = [] 12 | 13 | fetch("https://raw.githubusercontent.com/MeteorDevelopment/pulsar/master/properties.json") 14 | .then(res => res.json()) 15 | .then(res => res as Property[]) 16 | .then(res => res.forEach(property => { 17 | let item = new vscode.CompletionItem(property.name, vscode.CompletionItemKind.Property) 18 | 19 | item.insertText = property.name + ": " 20 | item.detail = property.type 21 | item.documentation = property.description 22 | 23 | completionItems.push(item) 24 | })) 25 | 26 | vscode.languages.registerCompletionItemProvider("pts", { 27 | provideCompletionItems(document, position, token, context) { 28 | return completionItems 29 | } 30 | }) 31 | } 32 | 33 | export function deactivate() {} 34 | -------------------------------------------------------------------------------- /pts-vscode/syntaxes/pts.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "scopeName": "source.pts", 4 | "name": "PTS", 5 | 6 | "patterns": [ 7 | { "include": "#line-comment" }, 8 | { "include": "#block-comment" }, 9 | 10 | { "include": "#color" }, 11 | { "include": "#number" }, 12 | { "include": "#string" }, 13 | 14 | { "include": "#at" }, 15 | { "include": "#selector" }, 16 | { "include": "#style" } 17 | ], 18 | 19 | "repository": { 20 | "line-comment": { 21 | "begin": "//", 22 | "end": "\n", 23 | "name": "comment.line.double-slash.css" 24 | }, 25 | "block-comment": { 26 | "begin": "/\\*", 27 | "end": "\\*/", 28 | "name": "comment.block.css" 29 | }, 30 | "at": { 31 | "match": "@[a-zA-Z0-9_-]+", 32 | "name": "keyword.control.at-rule.font-face.css" 33 | }, 34 | "selector": { 35 | "match": "\\.?[a-zA-Z0-9_-]+\\s*:?[a-zA-Z0-9_-]*", 36 | "name": "entity.name.tag.css" 37 | }, 38 | "style": { 39 | "begin": "{", 40 | "end": "}", 41 | "captures": { 42 | "0": { "name": "punctuation.section.property-list.begin.bracket.curly.css" } 43 | }, 44 | "patterns": [ 45 | { "include": "#line-comment" }, 46 | { "include": "#block-comment" }, 47 | 48 | { "include": "#at" }, 49 | { "include": "#property" }, 50 | { "include": "#color" }, 51 | { "include": "#number" }, 52 | { "include": "#string" } 53 | ] 54 | }, 55 | "property": { 56 | "match": "([a-zA-Z0-9_-]+)\\s*(:)", 57 | "captures": { 58 | "1": { "name": "support.type.property-name.css" }, 59 | "2": { "name": "punctuation.separator.key-value.css" } 60 | } 61 | }, 62 | "color": { 63 | "match": "#[a-fA-F0-9]+", 64 | "name": "constant.other.color.rgb-value.css" 65 | }, 66 | "number": { 67 | "match": "[0-9]+\\.?[0-9]*", 68 | "name": "keyword.other.unit.css" 69 | }, 70 | "string": { 71 | "begin": "\"", 72 | "end": "\"", 73 | "name": "string.quoted.double.css", 74 | "captures": { 75 | "0": { "name": "punctuation.definition.string.begin.css" } 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /pts-vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /pts/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java") 3 | id("java-library") 4 | id("antlr") 5 | id("maven-publish") 6 | } 7 | 8 | group = "org.meteordev" 9 | version = "0.1.0" 10 | 11 | var snapshot = true 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | // ANTLR 19 | antlr("org.antlr:antlr4:4.11.1") 20 | api("org.antlr:antlr4-runtime:4.11.1") 21 | 22 | // Jabel 23 | annotationProcessor("com.github.bsideup.jabel:jabel-javac-plugin:1.0.0") 24 | compileOnly("com.github.bsideup.jabel:jabel-javac-plugin:1.0.0") 25 | } 26 | 27 | configurations.api { 28 | exclude(group = "org.antlr", module = "antlr4") 29 | } 30 | 31 | sourceSets { 32 | create("generated") { 33 | java.srcDir("${projectDir}/src/generated/java") 34 | } 35 | 36 | main { 37 | java.srcDirs("${projectDir}/src/generated/java") 38 | } 39 | } 40 | 41 | tasks.withType { 42 | maxHeapSize = "64m" 43 | outputDirectory = file("${projectDir}/src/generated/java/org.meteordev/pts") 44 | 45 | arguments.add("-package") 46 | arguments.add("org.meteordev.pts") 47 | arguments.add("-visitor") 48 | } 49 | 50 | tasks.withType { 51 | dependsOn("generateGrammarSource") 52 | 53 | sourceCompatibility = JavaVersion.VERSION_17.toString() 54 | options.release.set(8) 55 | 56 | javaCompiler.set(javaToolchains.compilerFor { 57 | languageVersion.set(JavaLanguageVersion.of(17)) 58 | }) 59 | } 60 | 61 | tasks.named("clean") { 62 | delete("${projectDir}/src/generated") 63 | } 64 | 65 | publishing { 66 | publications { 67 | create("java") { 68 | version = "${project.version}${if (snapshot) "-SNAPSHOT" else ""}" 69 | 70 | from(components["java"]) 71 | } 72 | } 73 | 74 | repositories { 75 | maven { 76 | setUrl("https://maven.meteordev.org/${if (snapshot) "snapshots" else "releases"}") 77 | 78 | credentials { 79 | username = System.getenv("MAVEN_METEOR_ALIAS") 80 | password = System.getenv("MAVEN_METEOR_TOKEN") 81 | } 82 | 83 | authentication { 84 | create("basic") 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pts/src/main/antlr/Pts.g4: -------------------------------------------------------------------------------- 1 | grammar Pts; 2 | 3 | pts : statement* EOF ; 4 | 5 | // Statements 6 | statement : atStatement 7 | | style 8 | ; 9 | 10 | // At Statements 11 | atStatement : atTitle 12 | | atAuthors 13 | | atInclude 14 | | atVar 15 | | atMixin 16 | ; 17 | 18 | atTitle : '@title' title=STRING SEMICOLON ; 19 | atAuthors : '@authors' OPENING_BRACKET authors+=STRING (COMMA authors+=STRING)* CLOSING_BRACKET SEMICOLON ; 20 | atInclude : '@include' include=STRING SEMICOLON ; 21 | atVar : '@var' name=IDENTIFIER ':' type=IDENTIFIER '=' expression+ SEMICOLON ; 22 | atMixin : '@mixin' name=IDENTIFIER OPENING_BRACE properties+=declaration* CLOSING_BRACE ; 23 | 24 | // Style 25 | style : name=IDENTIFIER (('.' tag=IDENTIFIER) | (':' state=IDENTIFIER))* OPENING_BRACE declarations+=declaration* CLOSING_BRACE 26 | | (('.' tag=IDENTIFIER) | (':' state=IDENTIFIER))+ OPENING_BRACE declarations+=declaration* CLOSING_BRACE 27 | ; 28 | 29 | // Declarations 30 | declaration : apply 31 | | property 32 | ; 33 | 34 | apply : '@apply' name=IDENTIFIER SEMICOLON ; 35 | 36 | property : name=IDENTIFIER ':' expression+ SEMICOLON 37 | | name=IDENTIFIER '.' accessor=IDENTIFIER ':' expression+ SEMICOLON 38 | ; 39 | 40 | // Expressions 41 | expression : unit 42 | | color 43 | | identifier 44 | | string 45 | | variable 46 | | function 47 | ; 48 | 49 | unit : NUMBER PX ; 50 | color : HEX_COLOR ; 51 | identifier : IDENTIFIER ; 52 | string : STRING ; 53 | variable : BANG name=IDENTIFIER ; 54 | function : name=IDENTIFIER OPENING_PAREN args+=NUMBER? (COMMA args+=NUMBER*)* CLOSING_PAREN ; 55 | 56 | 57 | // Lexer 58 | NUMBER : '-'? INT ('.' INT)? ; 59 | STRING : QUOTE ~[\\"]* QUOTE ; 60 | 61 | HEX_COLOR : '#' HEX HEX HEX HEX HEX HEX 62 | | '#' HEX HEX HEX HEX HEX HEX HEX HEX 63 | ; 64 | 65 | PX : 'px' ; 66 | 67 | IDENTIFIER : [a-zA-Z_\-][a-zA-Z_\-0-9]* ; 68 | 69 | OPENING_PAREN : '(' ; 70 | CLOSING_PAREN : ')' ; 71 | 72 | OPENING_BRACE : '{' ; 73 | CLOSING_BRACE : '}' ; 74 | 75 | OPENING_BRACKET : '[' ; 76 | CLOSING_BRACKET : ']' ; 77 | 78 | COMMA : ',' ; 79 | SEMICOLON : ';' ; 80 | BANG : '!' ; 81 | QUOTE : '"' ; 82 | 83 | COMMENT : '/*' .*? '*/' -> channel(HIDDEN) ; 84 | LINE_COMMENT : '//' ~[\r\n]* -> channel(HIDDEN) ; 85 | 86 | WS : [ \n\t\r]+ -> channel(HIDDEN); 87 | UNKNOWN : . ; 88 | 89 | fragment INT : [0-9]+ ; 90 | fragment HEX : [0-9a-fA-F] ; -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/properties/Properties.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.properties; 2 | 3 | import org.meteordev.pts.utils.*; 4 | 5 | import java.util.Collection; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import static org.meteordev.pts.properties.PropertyTypes.*; 10 | 11 | public class Properties { 12 | private static final Map> PROPERTIES = new HashMap<>(); 13 | 14 | public static final Property COLOR = add(new Property<>("color", COLOR4_TYPE)); 15 | public static final Property BACKGROUND_COLOR = add(new Property<>("background-color", COLOR4_TYPE)); 16 | 17 | public static final Property OUTLINE_SIZE = add(new Property<>("outline-size", UNIT_TYPE)); 18 | public static final Property OUTLINE_COLOR = add(new Property<>("outline-color", COLOR4_TYPE)); 19 | 20 | public static final Property SPACING = add(new Property<>("spacing", VEC2_TYPE)); 21 | public static final Property PADDING = add(new Property<>("padding", VEC4_TYPE)); 22 | public static final Property RADIUS = add(new Property<>("radius", VEC4_TYPE)); 23 | public static final Property SIZE = add(new Property<>("size", VEC2_TYPE)); 24 | public static final Property MINIMUM_SIZE = add(new Property<>("minimum-size", VEC2_TYPE)); 25 | 26 | public static final Property ALIGN_X = add(new Property<>("align-x", ALIGN_X_TYPE)); 27 | public static final Property ALIGN_Y = add(new Property<>("align-y", ALIGN_Y_TYPE)); 28 | 29 | public static final Property FONT = add(new Property<>("font", STRING_TYPE)); 30 | public static final Property FONT_SIZE = add(new Property<>("font-size", UNIT_TYPE)); 31 | public static final Property TEXT_SHADOW = add(new Property<>("text-shadow", COLOR4_TYPE)); 32 | public static final Property TEXT_SHADOW_OFFSET = add(new Property<>("text-shadow-offset", VEC2_TYPE, new Vec2(1, -1))); 33 | 34 | public static final Property MAX_WIDTH = add(new Property<>("max-width", UNIT_TYPE, Double.MAX_VALUE)); 35 | public static final Property MAX_HEIGHT = add(new Property<>("max-height", UNIT_TYPE, Double.MAX_VALUE)); 36 | public static final Property OVERFLOW_Y = add(new Property<>("overflow-y", OVERFLOW_TYPE)); 37 | 38 | public static final Property LIST_DIRECTION = add(new Property<>("list-direction", LIST_DIRECTION_TYPE)); 39 | 40 | public static final Property ICON = add(new Property<>("icon", STRING_TYPE)); 41 | public static final Property ICON_PATH = add(new Property<>("icon-path", STRING_TYPE)); 42 | 43 | private static Property add(Property property) { 44 | PROPERTIES.put(property.name(), property); 45 | return property; 46 | } 47 | 48 | public static Property get(String name) { 49 | return PROPERTIES.get(name); 50 | } 51 | 52 | 53 | public static Collection> getAll() { 54 | return PROPERTIES.values(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/properties/Property.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.properties; 2 | 3 | import com.github.bsideup.jabel.Desugar; 4 | 5 | @Desugar 6 | public record Property(String name, PropertyType type, T defaultValue) { 7 | public Property(String name, PropertyType type) { 8 | this(name, type, null); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/properties/PropertyAccessor.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.properties; 2 | 3 | import com.github.bsideup.jabel.Desugar; 4 | 5 | import java.util.List; 6 | 7 | @Desugar 8 | public record PropertyAccessor(String name, ValueType[] types, Setter setter) { 9 | public boolean matches(String name, List types) { 10 | if (!this.name.equals(name)) return false; 11 | if (types.size() != this.types.length) return false; 12 | 13 | for (int i = 0; i < types.size(); i++) { 14 | if (types.get(i) != this.types[i]) return false; 15 | } 16 | 17 | return true; 18 | } 19 | 20 | public void set(T target, List values) { 21 | setter.set(target, values.toArray()); 22 | } 23 | 24 | public interface Setter { 25 | void set(T target, Object[] values); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/properties/PropertyConstructor.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.properties; 2 | 3 | import com.github.bsideup.jabel.Desugar; 4 | 5 | import java.util.List; 6 | 7 | @Desugar 8 | public record PropertyConstructor(ValueType[] types, Factory factory) { 9 | public boolean matches(List types) { 10 | if (types.size() != this.types.length) return false; 11 | 12 | for (int i = 0; i < types.size(); i++) { 13 | if (types.get(i) != this.types[i]) return false; 14 | } 15 | 16 | return true; 17 | } 18 | 19 | public T create(List values) { 20 | return factory.create(values.toArray()); 21 | } 22 | 23 | public interface Factory { 24 | T create(Object[] values); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/properties/PropertyType.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.properties; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Supplier; 6 | 7 | public class PropertyType { 8 | public final String name; 9 | public final Supplier accessorDefaultValue; 10 | public final T defaultValue; 11 | 12 | private final PropertyConstructor[] constructors; 13 | private final PropertyAccessor[] accessors; 14 | private final Decomposer decomposer; 15 | 16 | private PropertyType(String name, Supplier accessorDefaultValue, T defaultValue, PropertyConstructor[] constructors, PropertyAccessor[] accessors, Decomposer decomposer) { 17 | this.name = name; 18 | this.accessorDefaultValue = accessorDefaultValue; 19 | this.defaultValue = defaultValue; 20 | this.constructors = constructors; 21 | this.accessors = accessors; 22 | this.decomposer = decomposer; 23 | } 24 | 25 | public PropertyConstructor getConstructor(List types) { 26 | for (PropertyConstructor constructor : constructors) { 27 | if (constructor.matches(types)) return constructor; 28 | } 29 | 30 | return null; 31 | } 32 | 33 | public PropertyAccessor getAccessor(String name, List types) { 34 | for (PropertyAccessor accessor : accessors) { 35 | if (accessor.matches(name, types)) return accessor; 36 | } 37 | 38 | return null; 39 | } 40 | 41 | public void decompose(T value, List types, List values) { 42 | decomposer.decompose(value, types, values); 43 | } 44 | 45 | public static class Builder { 46 | private final String name; 47 | private final Supplier accessorDefaultValue; 48 | private T defaultValue = null; 49 | private final List> constructors = new ArrayList<>(); 50 | private final List> accessors = new ArrayList<>(); 51 | private Decomposer decomposer; 52 | 53 | public Builder(String name, Supplier accessorDefaultValue) { 54 | this.name = name; 55 | this.accessorDefaultValue = accessorDefaultValue; 56 | } 57 | 58 | public Builder defaultValue(T defaultValue) { 59 | this.defaultValue = defaultValue; 60 | return this; 61 | } 62 | 63 | public Builder constructor(PropertyConstructor.Factory factory, ValueType... types) { 64 | constructors.add(new PropertyConstructor<>(types, factory)); 65 | return this; 66 | } 67 | 68 | public Builder accessor(String name, PropertyAccessor.Setter setter, ValueType... types) { 69 | accessors.add(new PropertyAccessor<>(name, types, setter)); 70 | return this; 71 | } 72 | 73 | public Builder decomposer(Decomposer decomposer) { 74 | this.decomposer = decomposer; 75 | return this; 76 | } 77 | 78 | @SuppressWarnings("unchecked") 79 | public PropertyType build() { 80 | return new PropertyType<>( 81 | name, 82 | accessorDefaultValue, 83 | defaultValue, 84 | constructors.toArray(new PropertyConstructor[0]), 85 | accessors.toArray(new PropertyAccessor[0]), 86 | decomposer 87 | ); 88 | } 89 | } 90 | 91 | public interface Decomposer { 92 | void decompose(T value, List types, List values); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/properties/ValueType.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.properties; 2 | 3 | public enum ValueType { 4 | Unit, 5 | Identifier, 6 | Color, 7 | String 8 | } 9 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/utils/AlignX.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.utils; 2 | 3 | public enum AlignX { 4 | Left, 5 | Center, 6 | Right 7 | } 8 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/utils/AlignY.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.utils; 2 | 3 | public enum AlignY { 4 | Bottom, 5 | Center, 6 | Top 7 | } 8 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/utils/Color4.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.utils; 2 | 3 | public final class Color4 { 4 | public IColor topLeft, topRight, bottomRight, bottomLeft; 5 | 6 | public Color4(IColor topLeft, IColor topRight, IColor bottomRight, IColor bottomLeft) { 7 | this.topLeft = topLeft; 8 | this.topRight = topRight; 9 | this.bottomRight = bottomRight; 10 | this.bottomLeft = bottomLeft; 11 | } 12 | 13 | public Color4(IColor color) { 14 | this(color, color, color, color); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/utils/ColorFactory.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.utils; 2 | 3 | public class ColorFactory { 4 | public interface IColorFactory { 5 | IColor create(int r, int g, int b, int a); 6 | } 7 | 8 | public static IColorFactory factory = ColorImpl::new; 9 | 10 | public static IColor create(int r, int g, int b, int a) { 11 | return factory.create( 12 | clamp(r), 13 | clamp(g), 14 | clamp(b), 15 | clamp(a) 16 | ); 17 | } 18 | 19 | private static int clamp(int value) { 20 | if (value < 0) return 0; 21 | return Math.min(value, 255); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/utils/ColorImpl.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.utils; 2 | 3 | import com.github.bsideup.jabel.Desugar; 4 | 5 | @Desugar 6 | public record ColorImpl(int r, int g, int b, int a) implements IColor { 7 | @Override 8 | public int getR() { 9 | return r; 10 | } 11 | 12 | @Override 13 | public int getG() { 14 | return g; 15 | } 16 | 17 | @Override 18 | public int getB() { 19 | return b; 20 | } 21 | 22 | @Override 23 | public int getA() { 24 | return a; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/utils/IColor.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.utils; 2 | 3 | public interface IColor { 4 | IColor WHITE = new ColorImpl(255, 255, 255, 255); 5 | IColor BLACK = new ColorImpl(0, 0, 0, 255); 6 | 7 | int getR(); 8 | int getG(); 9 | int getB(); 10 | int getA(); 11 | } 12 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/utils/ListDirection.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.utils; 2 | 3 | public enum ListDirection { 4 | Normal, 5 | Reversed 6 | } 7 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/utils/Overflow.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.utils; 2 | 3 | public enum Overflow { 4 | Visible, 5 | Scroll 6 | } 7 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/utils/Vec2.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.utils; 2 | 3 | public class Vec2 { 4 | public double x, y; 5 | 6 | public Vec2(double x, double y) { 7 | this.x = x; 8 | this.y = y; 9 | } 10 | 11 | public Vec2(double v) { 12 | this(v, v); 13 | } 14 | 15 | public int intX() { 16 | return (int) Math.ceil(x); 17 | } 18 | 19 | public int intY() { 20 | return (int) Math.ceil(y); 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "[" + x + ", " + y + "]"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pts/src/main/java/org/meteordev/pts/utils/Vec4.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pts.utils; 2 | 3 | public class Vec4 { 4 | public double x, y, z, w; 5 | 6 | public Vec4(double x, double y, double z, double w) { 7 | this.x = x; 8 | this.y = y; 9 | this.z = z; 10 | this.w = w; 11 | } 12 | 13 | public Vec4(double v) { 14 | this(v, v, v, v); 15 | } 16 | 17 | public int top() { 18 | return (int) Math.ceil(x); 19 | } 20 | public void top(double v) { 21 | this.x = v; 22 | } 23 | 24 | public int right() { 25 | return (int) Math.ceil(y); 26 | } 27 | public void right(double v) { 28 | this.y = v; 29 | } 30 | 31 | public int bottom() { 32 | return (int) Math.ceil(z); 33 | } 34 | public void bottom(double v) { 35 | this.z = v; 36 | } 37 | 38 | public int left() { 39 | return (int) Math.ceil(w); 40 | } 41 | public void left(double v) { 42 | this.w = v; 43 | } 44 | 45 | public int horizontal() { 46 | return (int) Math.ceil(left() + right()); 47 | } 48 | public void horizontal(double v) { 49 | left(v); 50 | right(v); 51 | } 52 | 53 | public int vertical() { 54 | return (int) Math.ceil(bottom() + top()); 55 | } 56 | public void vertical(double v) { 57 | bottom(v); 58 | top(v); 59 | } 60 | 61 | public double topLeft() { 62 | return x; 63 | } 64 | public void topLeft(double v) { 65 | this.x = v; 66 | } 67 | 68 | public double topRight() { 69 | return y; 70 | } 71 | public void topRight(double v) { 72 | this.y = v; 73 | } 74 | 75 | public double bottomRight() { 76 | return z; 77 | } 78 | public void bottomRight(double v) { 79 | this.z = v; 80 | } 81 | 82 | public double bottomLeft() { 83 | return w; 84 | } 85 | public void bottomLeft(double v) { 86 | this.w = v; 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return "[" + x + ", " + y + ", " + z + ", " + w + "]"; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pulsar/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java") 3 | id("java-library") 4 | id("maven-publish") 5 | } 6 | 7 | group = "org.meteordev" 8 | version = "0.1.0" 9 | 10 | var snapshot = true 11 | 12 | repositories { 13 | mavenCentral() 14 | 15 | maven { 16 | name = "Meteor - Snapshots" 17 | setUrl("https://maven.meteordev.org/snapshots") 18 | } 19 | } 20 | 21 | dependencies { 22 | api(project(":pts")) 23 | implementation(project(":pts")) 24 | 25 | compileOnly(platform("org.lwjgl:lwjgl-bom:3.3.1")) 26 | 27 | compileOnly("org.lwjgl:lwjgl") 28 | compileOnly("org.lwjgl:lwjgl-glfw") 29 | compileOnly("org.lwjgl:lwjgl-stb") 30 | compileOnly("org.lwjgl:lwjgl-nanovg") 31 | 32 | compileOnly("org.joml:joml:1.10.5") 33 | compileOnly("org.meteordev:juno-api:0.1.0-SNAPSHOT") 34 | 35 | annotationProcessor("com.github.bsideup.jabel:jabel-javac-plugin:1.0.0") 36 | compileOnly("com.github.bsideup.jabel:jabel-javac-plugin:1.0.0") 37 | } 38 | 39 | tasks.withType { 40 | sourceCompatibility = JavaVersion.VERSION_17.toString() 41 | options.release.set(8) 42 | 43 | javaCompiler.set(javaToolchains.compilerFor { 44 | languageVersion.set(JavaLanguageVersion.of(17)) 45 | }) 46 | } 47 | 48 | tasks.withType { 49 | dependsOn(project(":pts").tasks.withType()) 50 | 51 | from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) 52 | } 53 | 54 | publishing { 55 | publications { 56 | create("java") { 57 | version = "${project.version}${if (snapshot) "-SNAPSHOT" else ""}" 58 | 59 | from(components["java"]) 60 | } 61 | } 62 | 63 | repositories { 64 | maven { 65 | setUrl("https://maven.meteordev.org/${if (snapshot) "snapshots" else "releases"}") 66 | 67 | credentials { 68 | username = System.getenv("MAVEN_METEOR_ALIAS") 69 | password = System.getenv("MAVEN_METEOR_TOKEN") 70 | } 71 | 72 | authentication { 73 | create("basic") 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/input/CharTypedEvent.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.input; 2 | 3 | /** Input event representing character being typed. */ 4 | public class CharTypedEvent extends UsableEvent { 5 | public char c; 6 | 7 | public CharTypedEvent() { 8 | super(EventType.CharTyped); 9 | } 10 | 11 | /** Prepares this event for a dispatch. */ 12 | public CharTypedEvent set(char c) { 13 | this.c = c; 14 | this.used = false; 15 | 16 | return this; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/input/Event.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.input; 2 | 3 | /** Base class for all input events. */ 4 | public abstract class Event { 5 | public EventType type; 6 | 7 | public Event(EventType type) { 8 | this.type = type; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/input/EventHandler.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.input; 2 | 3 | /** Base class for all objects that can receive user input. */ 4 | public abstract class EventHandler { 5 | /** Dispatches an input event to this widget and all of its children. */ 6 | public void dispatch(Event event) { 7 | switch (event.type) { 8 | case MousePressed -> onMousePressed((MouseButtonEvent) event); 9 | case MouseMoved -> onMouseMoved((MouseMovedEvent) event); 10 | case MouseReleased -> onMouseReleased((MouseButtonEvent) event); 11 | case MouseScrolled -> onMouseScrolled((MouseScrolledEvent) event); 12 | 13 | case KeyPressed -> onKeyPressed((KeyEvent) event); 14 | case KeyRepeated -> onKeyRepeated((KeyEvent) event); 15 | case CharTyped -> onCharTyped((CharTypedEvent) event); 16 | } 17 | } 18 | 19 | protected void onMousePressed(MouseButtonEvent event) {} 20 | protected void onMouseMoved(MouseMovedEvent event) {} 21 | protected void onMouseReleased(MouseButtonEvent event) {} 22 | protected void onMouseScrolled(MouseScrolledEvent event) {} 23 | 24 | protected void onKeyPressed(KeyEvent event) {} 25 | protected void onKeyRepeated(KeyEvent event) {} 26 | protected void onCharTyped(CharTypedEvent event) {} 27 | } 28 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/input/EventType.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.input; 2 | 3 | /** Determines an input even type. */ 4 | public enum EventType { 5 | MousePressed, 6 | MouseMoved, 7 | MouseReleased, 8 | MouseScrolled, 9 | 10 | KeyPressed, 11 | KeyRepeated, 12 | CharTyped 13 | } 14 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/input/KeyEvent.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.input; 2 | 3 | /** Input event representing key being pressed or repeated. */ 4 | public class KeyEvent extends UsableEvent { 5 | public int key, mods; 6 | 7 | public KeyEvent() { 8 | super(EventType.KeyPressed); 9 | } 10 | 11 | /** Prepares this event for a dispatch. */ 12 | public KeyEvent set(EventType type, int key, int mods) { 13 | this.type = type; 14 | this.key = key; 15 | this.mods = mods; 16 | this.used = false; 17 | 18 | return this; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/input/MouseButtonEvent.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.input; 2 | 3 | /** Input event representing mouse button being pressed or released. */ 4 | public class MouseButtonEvent extends UsableEvent { 5 | public double x, y; 6 | public int button; 7 | 8 | public MouseButtonEvent() { 9 | super(EventType.MousePressed); 10 | } 11 | 12 | /** Prepares this event for a dispatch. */ 13 | public MouseButtonEvent set(EventType type, double x, double y, int button) { 14 | this.type = type; 15 | this.x = x; 16 | this.y = y; 17 | this.button = button; 18 | this.used = false; 19 | 20 | return this; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/input/MouseMovedEvent.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.input; 2 | 3 | /** Input event representing mouse being moved. */ 4 | public class MouseMovedEvent extends Event { 5 | public double x, y; 6 | public double deltaX, deltaY; 7 | 8 | public MouseMovedEvent() { 9 | super(EventType.MouseMoved); 10 | } 11 | 12 | /** Prepares this event for a dispatch. */ 13 | public MouseMovedEvent set(double x, double y, double deltaX, double deltaY) { 14 | this.x = x; 15 | this.y = y; 16 | this.deltaX = deltaX; 17 | this.deltaY = deltaY; 18 | 19 | return this; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/input/MouseScrolledEvent.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.input; 2 | 3 | /** Input event representing mouse wheel being scrolled. */ 4 | public class MouseScrolledEvent extends UsableEvent { 5 | public double value; 6 | 7 | public MouseScrolledEvent() { 8 | super(EventType.MouseScrolled); 9 | } 10 | 11 | /** Prepares this event for a dispatch. */ 12 | public MouseScrolledEvent set(double value) { 13 | this.value = value; 14 | this.used = false; 15 | 16 | return this; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/input/UsableEvent.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.input; 2 | 3 | public abstract class UsableEvent extends Event { 4 | public boolean used; 5 | 6 | public UsableEvent(EventType type) { 7 | super(type); 8 | } 9 | 10 | /** Sets the {@link #used} field to true. */ 11 | public void use() { 12 | used = true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/layout/BasicLayout.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.layout; 2 | 3 | import org.meteordev.pts.properties.Properties; 4 | import org.meteordev.pts.utils.Vec4; 5 | import org.meteordev.pulsar.widgets.Cell; 6 | import org.meteordev.pulsar.widgets.Widget; 7 | 8 | /** Basic layout which positions all widgets on top of each other. */ 9 | public class BasicLayout extends Layout { 10 | public static final BasicLayout INSTANCE = new BasicLayout(); 11 | 12 | protected BasicLayout() {} 13 | 14 | @Override 15 | protected void calculateSizeImpl(Widget widget) { 16 | Vec4 padding = widget.get(Properties.PADDING); 17 | 18 | for (Cell cell : widget) { 19 | if (cell.widget().shouldSkipLayout()) continue; 20 | 21 | widget.width = Math.max(widget.width, cell.widget().width + padding.horizontal()); 22 | widget.height = Math.max(widget.height, cell.widget().height + padding.vertical()); 23 | } 24 | } 25 | 26 | @Override 27 | public void positionChildrenImpl(Widget widget) { 28 | Vec4 padding = widget.get(Properties.PADDING); 29 | 30 | for (Cell cell : widget) { 31 | if (cell.widget().shouldSkipLayout()) continue; 32 | 33 | cell.x = padding.left() + widget.x; 34 | cell.y = padding.top() + widget.y; 35 | 36 | cell.width = widget.width - padding.horizontal(); 37 | cell.height = widget.height - padding.vertical(); 38 | 39 | cell.align(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/layout/HorizontalLayout.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.layout; 2 | 3 | import org.meteordev.pts.properties.Properties; 4 | import org.meteordev.pts.utils.ListDirection; 5 | import org.meteordev.pts.utils.Vec2; 6 | import org.meteordev.pts.utils.Vec4; 7 | import org.meteordev.pulsar.widgets.Cell; 8 | import org.meteordev.pulsar.widgets.Widget; 9 | 10 | /** Layout which positions all widgets horizontally next to each other. */ 11 | public class HorizontalLayout extends Layout { 12 | private int expandCellCount; 13 | private double calculatedWidth; 14 | 15 | @Override 16 | protected void calculateSizeImpl(Widget widget) { 17 | Vec4 padding = widget.get(Properties.PADDING); 18 | Vec2 spacing = widget.get(Properties.SPACING); 19 | 20 | expandCellCount = 0; 21 | 22 | for (Widget.CellIterator it = widget.iterator(false); it.hasNext();) { 23 | Cell cell = it.next(); 24 | if (cell.widget().shouldSkipLayout()) continue; 25 | 26 | if (it.isNotFirst()) widget.width += spacing.intX(); 27 | 28 | widget.width += cell.widget().width; 29 | widget.height = Math.max(widget.height, cell.widget().height + padding.vertical()); 30 | 31 | if (cell.expandCellX) expandCellCount++; 32 | } 33 | 34 | widget.width += padding.horizontal(); 35 | 36 | calculatedWidth = widget.width; 37 | } 38 | 39 | @Override 40 | public void positionChildrenImpl(Widget widget) { 41 | Vec4 padding = widget.get(Properties.PADDING); 42 | Vec2 spacing = widget.get(Properties.SPACING); 43 | boolean reversed = widget.get(Properties.LIST_DIRECTION) == ListDirection.Reversed; 44 | 45 | int x = widget.x + padding.left(); 46 | 47 | double expandWidthD = (widget.width - calculatedWidth) / expandCellCount; 48 | int expandWidth = (int) expandWidthD; 49 | Cell lastExpandWidth = null; 50 | 51 | for (Widget.CellIterator it = widget.iterator(reversed); it.hasNext();) { 52 | Cell cell = it.next(); 53 | if (cell.widget().shouldSkipLayout()) continue; 54 | 55 | cell.x = x; 56 | cell.y = padding.top() + widget.y; 57 | 58 | cell.width = cell.widget().width; 59 | cell.height = widget.height - padding.vertical(); 60 | 61 | if (cell.expandCellX) { 62 | cell.width += expandWidth; 63 | lastExpandWidth = cell; 64 | } 65 | 66 | cell.align(); 67 | 68 | x += cell.width + spacing.intX(); 69 | } 70 | 71 | if (lastExpandWidth != null) { 72 | lastExpandWidth.width += (int) Math.ceil((expandWidthD - expandWidth) * expandCellCount); 73 | lastExpandWidth.align(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/layout/Layout.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.layout; 2 | 3 | import org.meteordev.pts.properties.Properties; 4 | import org.meteordev.pts.utils.Vec2; 5 | import org.meteordev.pulsar.widgets.Cell; 6 | import org.meteordev.pulsar.widgets.Widget; 7 | 8 | /** Base class for all layouts */ 9 | public abstract class Layout { 10 | /** Calculates size for this specified widget and all its children. */ 11 | public void calculateSize(Widget widget) { 12 | for (Cell cell : widget) cell.widget().layout.calculateSize(cell.widget()); 13 | 14 | widget.width = widget.height = 0; 15 | 16 | if (widget.hasChildren()) calculateSizeImpl(widget); 17 | else widget.calculateSize(); 18 | 19 | Vec2 minSize = getMinSize(widget); 20 | if (minSize != null) { 21 | widget.width = Math.max(widget.width, minSize.intX()); 22 | widget.height = Math.max(widget.height, minSize.intY()); 23 | } 24 | } 25 | 26 | /** The actual implementation for size calculation. */ 27 | protected abstract void calculateSizeImpl(Widget widget); 28 | 29 | protected Vec2 getMinSize(Widget widget) { 30 | return widget.get(Properties.MINIMUM_SIZE); 31 | } 32 | 33 | /** Position's children widgets and its children according to this layout. */ 34 | public void positionChildren(Widget widget) { 35 | positionChildrenImpl(widget); 36 | 37 | for (Cell cell : widget) cell.widget().layout.positionChildren(cell.widget()); 38 | } 39 | 40 | /** The actual implementation for positioning children. */ 41 | protected abstract void positionChildrenImpl(Widget widget); 42 | 43 | /** Tries to satisfy max-width and max-height properties. */ 44 | public void adjustToMaxSize(MaxSizeCalculationContext ctx, Widget widget) { 45 | ctx.pushMaxSize(widget); 46 | 47 | for (Cell cell : widget) cell.widget().layout.adjustToMaxSize(ctx, cell.widget()); 48 | 49 | if (widget.adjustToMaxSize(ctx)) ctx.setAdjusted(); 50 | 51 | ctx.popMaxSize(); 52 | } 53 | 54 | /** Last step in the layout calculation process. */ 55 | public void afterLayout(Widget widget) { 56 | widget.afterLayout(); 57 | 58 | for (Cell cell : widget) cell.widget().layout.afterLayout(cell.widget()); 59 | } 60 | 61 | /** Called when a cell is added to widget. */ 62 | public void onAdd(Widget widget, Cell cell) {} 63 | 64 | /** Called when all widget's children are removed. */ 65 | public void onClear(Widget widget) {} 66 | } 67 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/layout/MaxSizeCalculationContext.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.layout; 2 | 3 | import org.meteordev.pts.properties.Properties; 4 | import org.meteordev.pulsar.utils.IntStack; 5 | import org.meteordev.pulsar.widgets.Widget; 6 | 7 | public class MaxSizeCalculationContext { 8 | private final IntStack xStack = new IntStack(10); 9 | private final IntStack maxWidthStack = new IntStack(10); 10 | 11 | private final IntStack yStack = new IntStack(10); 12 | private final IntStack maxHeightStack = new IntStack(10); 13 | 14 | private boolean adjusted; 15 | 16 | public MaxSizeCalculationContext() { 17 | xStack.push(Integer.MIN_VALUE); 18 | maxWidthStack.push(Integer.MAX_VALUE); 19 | 20 | yStack.push(Integer.MIN_VALUE); 21 | maxHeightStack.push(Integer.MAX_VALUE); 22 | } 23 | 24 | public void pushMaxSize(Widget widget) { 25 | // Max width 26 | int maxWidth = maxWidthStack.peek(); 27 | double widgetMaxWidth = widget.get(Properties.MAX_WIDTH); 28 | 29 | boolean hasMaxWidth = maxWidth != Integer.MAX_VALUE; 30 | boolean widgetHasMaxWidth = widgetMaxWidth != Double.MAX_VALUE; 31 | 32 | int x = xStack.peek(); 33 | xStack.push(Math.max(xStack.peek(), widget.x)); 34 | 35 | if (!hasMaxWidth) { 36 | maxWidthStack.push((int) widgetMaxWidth); 37 | } 38 | else { 39 | int newMaxWidth = maxWidth - (xStack.peek() - x) - widget.get(Properties.PADDING).right(); // I am not sure if the right padding here is correct 40 | if (widgetHasMaxWidth) newMaxWidth = Math.min(newMaxWidth, (int) widgetMaxWidth); 41 | maxWidthStack.push(newMaxWidth); 42 | } 43 | 44 | // Max height 45 | int maxHeight = maxHeightStack.peek(); 46 | double widgetMaxHeight = widget.get(Properties.MAX_HEIGHT); 47 | 48 | boolean hasMaxHeight = maxHeight != Integer.MAX_VALUE; 49 | boolean widgetHasMaxHeight = widgetMaxHeight != Double.MAX_VALUE; 50 | 51 | int y = yStack.peek(); 52 | yStack.push(Math.max(yStack.peek(), widget.y)); 53 | 54 | if (!hasMaxHeight) { 55 | maxHeightStack.push((int) widgetMaxHeight); 56 | } 57 | else { 58 | int newMaxHeight = maxHeight - (yStack.peek() - y) - widget.get(Properties.PADDING).bottom(); // I am not sure if the bottom padding here is correct 59 | if (widgetHasMaxHeight) newMaxHeight = Math.min(newMaxHeight, (int) widgetMaxHeight); 60 | maxHeightStack.push(newMaxHeight); 61 | } 62 | } 63 | 64 | public void popMaxSize() { 65 | xStack.pop(); 66 | maxWidthStack.pop(); 67 | 68 | yStack.pop(); 69 | maxHeightStack.pop(); 70 | } 71 | 72 | public int peekMaxWidth() { 73 | return maxWidthStack.peek(); 74 | } 75 | 76 | public int peekMaxHeight() { 77 | return maxHeightStack.peek(); 78 | } 79 | 80 | public void setAdjusted() { 81 | adjusted = true; 82 | } 83 | 84 | public boolean wasAdjusted() { 85 | return adjusted; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/layout/VerticalLayout.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.layout; 2 | 3 | import org.meteordev.pts.properties.Properties; 4 | import org.meteordev.pts.utils.ListDirection; 5 | import org.meteordev.pts.utils.Vec2; 6 | import org.meteordev.pts.utils.Vec4; 7 | import org.meteordev.pulsar.widgets.Cell; 8 | import org.meteordev.pulsar.widgets.Widget; 9 | 10 | /** Layout which positions all widgets vertically next to each other. */ 11 | public class VerticalLayout extends Layout { 12 | public static final VerticalLayout INSTANCE = new VerticalLayout(); 13 | 14 | protected VerticalLayout() {} 15 | 16 | @Override 17 | protected void calculateSizeImpl(Widget widget) { 18 | Vec4 padding = widget.get(Properties.PADDING); 19 | Vec2 spacing = widget.get(Properties.SPACING); 20 | 21 | for (Widget.CellIterator it = widget.iterator(false); it.hasNext();) { 22 | Cell cell = it.next(); 23 | if (cell.widget().shouldSkipLayout()) continue; 24 | 25 | if (it.isNotFirst()) widget.height += spacing.intY(); 26 | 27 | widget.width = Math.max(widget.width, cell.widget().width + padding.horizontal()); 28 | widget.height += cell.widget().height; 29 | } 30 | 31 | widget.height += padding.vertical(); 32 | } 33 | 34 | @Override 35 | public void positionChildrenImpl(Widget widget) { 36 | Vec4 padding = widget.get(Properties.PADDING); 37 | Vec2 spacing = widget.get(Properties.SPACING); 38 | boolean reversed = widget.get(Properties.LIST_DIRECTION) == ListDirection.Reversed; 39 | 40 | int y = widget.y + padding.top(); 41 | 42 | for (Widget.CellIterator it = widget.iterator(reversed); it.hasNext();) { 43 | Cell cell = it.next(); 44 | if (cell.widget().shouldSkipLayout()) continue; 45 | 46 | cell.x = padding.left() + widget.x; 47 | cell.y = y; 48 | 49 | cell.width = widget.width - padding.horizontal(); 50 | cell.height = cell.widget().height; 51 | 52 | cell.align(); 53 | 54 | y += cell.height + spacing.intY(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/rendering/CharData.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.rendering; 2 | 3 | import com.github.bsideup.jabel.Desugar; 4 | 5 | @Desugar 6 | public record CharData(float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float xAdvance) { 7 | } 8 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/rendering/DebugRenderer.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.rendering; 2 | 3 | import org.joml.Matrix4f; 4 | import org.meteordev.juno.api.Juno; 5 | import org.meteordev.juno.api.JunoProvider; 6 | import org.meteordev.juno.api.pipeline.Pipeline; 7 | import org.meteordev.juno.api.pipeline.PipelineInfo; 8 | import org.meteordev.juno.api.pipeline.PrimitiveType; 9 | import org.meteordev.juno.api.pipeline.state.WriteMask; 10 | import org.meteordev.juno.api.pipeline.vertexformat.StandardFormats; 11 | import org.meteordev.juno.api.shader.ShaderInfo; 12 | import org.meteordev.juno.api.shader.ShaderType; 13 | import org.meteordev.juno.api.utils.MeshBuilder; 14 | import org.meteordev.pts.utils.ColorFactory; 15 | import org.meteordev.pts.utils.IColor; 16 | import org.meteordev.pulsar.widgets.Cell; 17 | import org.meteordev.pulsar.widgets.Widget; 18 | 19 | public class DebugRenderer { 20 | private static final IColor CELL_COLOR = ColorFactory.create(25, 225, 25, 255); 21 | private static final IColor WIDGET_COLOR = ColorFactory.create(25, 25, 225, 255); 22 | 23 | private static Pipeline pipeline; 24 | private static MeshBuilder mb; 25 | 26 | private static void lazyInit() { 27 | if (pipeline == null) { 28 | Juno juno = JunoProvider.get(); 29 | 30 | pipeline = juno.findPipeline(new PipelineInfo() 31 | .setPrimitiveType(PrimitiveType.LINES) 32 | .setVertexFormat(StandardFormats.POSITION_2D_COLOR) 33 | .setShaders( 34 | ShaderInfo.resource(ShaderType.VERTEX, "/pulsar/shaders/basic.vert"), 35 | ShaderInfo.resource(ShaderType.FRAGMENT, "/pulsar/shaders/basic.frag") 36 | ) 37 | .setWriteMask(WriteMask.COLOR) 38 | ); 39 | 40 | mb = new MeshBuilder(pipeline.getInfo().vertexFormat); 41 | } 42 | } 43 | 44 | public static void render(Widget widget, int windowWidth, int windowHeight) { 45 | Juno juno = JunoProvider.get(); 46 | 47 | lazyInit(); 48 | mb.begin(); 49 | 50 | render(widget); 51 | 52 | juno.bind(pipeline); 53 | pipeline.getProgram().getMatrix4Uniform("u_Proj").set(new Matrix4f().ortho2D(0, windowWidth, windowHeight, 0)); 54 | mb.draw(); 55 | } 56 | 57 | private static void render(Widget widget) { 58 | lineBox(widget.x, widget.y, widget.width, widget.height, WIDGET_COLOR); 59 | 60 | for (Cell cell : widget) { 61 | lineBox(cell.x, cell.y, cell.width, cell.height, CELL_COLOR); 62 | render(cell.widget()); 63 | } 64 | } 65 | 66 | private static void lineBox(double x, double y, double width, double height, IColor color) { 67 | line(x, y, x + width, y, color); 68 | line(x + width, y, x + width, y + height, color); 69 | line(x, y, x, y + height, color); 70 | line(x, y + height, x + width, y + height, color); 71 | } 72 | 73 | private static void line(double x1, double y1, double x2, double y2, IColor color) { 74 | mb.line( 75 | mb.float2(x1, y1).color(color.getR(), color.getG(), color.getB(), color.getA()).next(), 76 | mb.float2(x2, y2).color(color.getR(), color.getG(), color.getB(), color.getA()).next() 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/rendering/FontInfo.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.rendering; 2 | 3 | import org.lwjgl.stb.STBTTFontinfo; 4 | import org.lwjgl.stb.STBTruetype; 5 | import org.lwjgl.system.MemoryUtil; 6 | 7 | import java.nio.ByteBuffer; 8 | 9 | public class FontInfo { 10 | public final STBTTFontinfo fontInfo; 11 | public final ByteBuffer buffer; 12 | 13 | public FontInfo(ByteBuffer buffer) { 14 | fontInfo = STBTTFontinfo.create(); 15 | STBTruetype.stbtt_InitFont(fontInfo, buffer); 16 | 17 | this.buffer = buffer; 18 | } 19 | 20 | public void dispose() { 21 | MemoryUtil.memFree(buffer); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/rendering/Icons.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.rendering; 2 | 3 | import org.lwjgl.nanovg.NSVGImage; 4 | import org.lwjgl.nanovg.NanoSVG; 5 | import org.lwjgl.system.MemoryStack; 6 | import org.lwjgl.system.MemoryUtil; 7 | import org.meteordev.juno.api.texture.Texture; 8 | import org.meteordev.pulsar.theme.Theme; 9 | 10 | import java.nio.ByteBuffer; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class Icons { 15 | private Theme theme; 16 | 17 | private final TextureAtlas atlas = new TextureAtlas(); 18 | private final Map> icons = new HashMap<>(); 19 | 20 | public void setTheme(Theme theme) { 21 | this.theme = theme; 22 | 23 | atlas.clear(); 24 | icons.clear(); 25 | } 26 | 27 | public TextureRegion get(String path, int size) { 28 | Map icons = this.icons.computeIfAbsent(path, s -> new HashMap<>()); 29 | 30 | TextureRegion region = icons.get(size); 31 | if (region != null) return region; 32 | 33 | long rast = NanoSVG.nsvgCreateRasterizer(); 34 | if (rast == MemoryUtil.NULL) 35 | throw new IllegalStateException("Failed to create SVG rasterizer"); 36 | ByteBuffer terminated = terminate(theme.readFile(path)); 37 | NSVGImage svg = NanoSVG.nsvgParse(terminated, MemoryStack.stackASCII("px"), 96f); 38 | ByteBuffer image = MemoryUtil.memAlloc(size * size * 4); 39 | NanoSVG.nsvgRasterize(rast, svg, 0, 0, size / Math.max(svg.height(), svg.width()), image, size, size, size * 4); 40 | NanoSVG.nsvgDeleteRasterizer(rast); 41 | region = atlas.add(image, size, size); 42 | MemoryUtil.memFree(image); 43 | NanoSVG.nsvgDelete(svg); 44 | MemoryUtil.memFree(terminated); 45 | 46 | icons.put(size, region); 47 | return region; 48 | } 49 | 50 | public Texture getTexture() { 51 | return atlas.getTexture(); 52 | } 53 | 54 | private ByteBuffer terminate(ByteBuffer string) { 55 | ByteBuffer result = MemoryUtil.memAlloc(string.capacity() + 1); 56 | result.clear(); 57 | result.put(string); 58 | result.put((byte) 0); 59 | result.rewind(); 60 | return result; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/rendering/TextureAtlas.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.rendering; 2 | 3 | import org.meteordev.juno.api.JunoProvider; 4 | import org.meteordev.juno.api.texture.Filter; 5 | import org.meteordev.juno.api.texture.Format; 6 | import org.meteordev.juno.api.texture.Texture; 7 | import org.meteordev.juno.api.texture.Wrap; 8 | 9 | import java.nio.ByteBuffer; 10 | 11 | import static org.lwjgl.system.MemoryUtil.*; 12 | 13 | public class TextureAtlas { 14 | private static final int SIZE = 256; 15 | 16 | private final Texture texture; 17 | private final ByteBuffer buffer; 18 | 19 | private boolean dirty; 20 | private int x, y; 21 | private int rowHeight; 22 | 23 | public TextureAtlas() { 24 | texture = JunoProvider.get().createTexture(SIZE, SIZE, Format.RGBA, Filter.LINEAR, Filter.LINEAR, Wrap.CLAMP_TO_BORDER); 25 | buffer = memAlloc(SIZE * SIZE * 4); 26 | } 27 | 28 | public void clear() { 29 | dirty = false; 30 | x = 0; 31 | y = 0; 32 | } 33 | 34 | public TextureRegion add(ByteBuffer regionBuffer, int width, int height) { 35 | checkPos(width, height); 36 | 37 | for (int i = 0; i < height; i++) { 38 | memCopy( 39 | memAddress(regionBuffer, i * width * 4), 40 | memAddress(buffer, ((y + i) * SIZE + x) * 4), 41 | width * 4L 42 | ); 43 | } 44 | 45 | TextureRegion region = new TextureRegion((double) x / SIZE, (double) (y + height) / SIZE, (double) (x + width) / SIZE, (double) y / SIZE); 46 | 47 | dirty = true; 48 | x += width; 49 | 50 | return region; 51 | } 52 | 53 | public Texture getTexture() { 54 | if (dirty) { 55 | texture.write(buffer); 56 | dirty = false; 57 | } 58 | 59 | return texture; 60 | } 61 | 62 | private void checkPos(int width, int height) { 63 | rowHeight = Math.max(rowHeight, height); 64 | 65 | if (x + width >= SIZE) { 66 | x = 0; 67 | y += rowHeight; 68 | rowHeight = 0; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/rendering/TextureRegion.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.rendering; 2 | 3 | import com.github.bsideup.jabel.Desugar; 4 | 5 | @Desugar 6 | public record TextureRegion(double x1, double y1, double x2, double y2) { 7 | } 8 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/theme/IStylable.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.theme; 2 | 3 | import java.util.List; 4 | 5 | public interface IStylable { 6 | /** @return all names of this object. */ 7 | String[] names(); 8 | 9 | /** @return all tags of this object. */ 10 | List tags(); 11 | 12 | /** @return true if this object is hovered. */ 13 | boolean isHovered(); 14 | 15 | /** @return true if this object is pressed. */ 16 | boolean isPressed(); 17 | } 18 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/theme/Selector.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.theme; 2 | 3 | import com.github.bsideup.jabel.Desugar; 4 | import org.meteordev.pulsar.utils.Lists; 5 | 6 | import java.util.List; 7 | 8 | @Desugar 9 | public record Selector(String[] names, List tags, boolean isHovered, boolean isPressed) implements IStylable { 10 | @Override 11 | public String[] names() { 12 | return names; 13 | } 14 | 15 | @Override 16 | public List tags() { 17 | return tags == null ? Lists.of() : tags; 18 | } 19 | 20 | @Override 21 | public boolean isHovered() { 22 | return isHovered; 23 | } 24 | 25 | @Override 26 | public boolean isPressed() { 27 | return isPressed; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/theme/Style.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.theme; 2 | 3 | import org.meteordev.pts.properties.Property; 4 | import org.meteordev.pulsar.utils.PropertyMap; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public class Style { 10 | public enum State { 11 | Normal, 12 | Hovered, 13 | Pressed; 14 | 15 | public static State of(String state) { 16 | if (state == null) return Normal; 17 | if (state.equalsIgnoreCase("hovered")) return Hovered; 18 | if (state.equalsIgnoreCase("pressed")) return Pressed; 19 | return Normal; 20 | } 21 | } 22 | 23 | public String name; 24 | public List tags; 25 | public State state = State.Normal; 26 | 27 | private final Map, Object> properties = new PropertyMap(); 28 | 29 | public void set(Property property, T value) { 30 | properties.put(property, value); 31 | } 32 | 33 | @SuppressWarnings("unchecked") 34 | public T getRaw(Property property) { 35 | return (T) properties.get(property); 36 | } 37 | 38 | @SuppressWarnings("unchecked") 39 | public T get(Property property) { 40 | T value = (T) properties.get(property); 41 | if (value == null) value = property.defaultValue(); 42 | return value != null ? value : property.type().defaultValue; 43 | } 44 | 45 | public void merge(Style style) { 46 | properties.putAll(style.properties); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "Style{" + 52 | "name='" + name + '\'' + 53 | ", tags=" + tags + 54 | ", state='" + state + '\'' + 55 | '}'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/theme/Theme.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.theme; 2 | 3 | import org.meteordev.pulsar.theme.fileresolvers.IFileResolver; 4 | import org.meteordev.pulsar.utils.Utils; 5 | import org.lwjgl.system.MemoryUtil; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.nio.ByteBuffer; 10 | import java.util.Collection; 11 | 12 | public class Theme { 13 | public String title; 14 | public Collection authors; 15 | 16 | private final Styles styles = new Styles(); 17 | 18 | private IFileResolver fileResolver; 19 | 20 | public void setFileResolver(IFileResolver fileResolver) { 21 | this.fileResolver = fileResolver; 22 | } 23 | 24 | public void addStyle(Style style) { 25 | styles.add(style); 26 | } 27 | 28 | public Style computeStyle(IStylable widget) { 29 | return styles.compute(widget); 30 | } 31 | 32 | public ByteBuffer readFile(String path) { 33 | InputStream in = fileResolver.get(path); 34 | if (in == null) throw new RuntimeException("Failed to read file '" + fileResolver.resolvePath(path) + "'."); 35 | 36 | byte[] bytes = Utils.read(in); 37 | ByteBuffer buffer = MemoryUtil.memAlloc(bytes.length); 38 | buffer.put(bytes).rewind(); 39 | 40 | try { 41 | in.close(); 42 | } catch (IOException e) { 43 | throw new RuntimeException(e); 44 | } 45 | 46 | return buffer; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/theme/fileresolvers/IFileResolver.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.theme.fileresolvers; 2 | 3 | import java.io.InputStream; 4 | 5 | public interface IFileResolver { 6 | String resolvePath(String path); 7 | 8 | InputStream get(String path); 9 | } 10 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/theme/fileresolvers/NormalFileResolver.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.theme.fileresolvers; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.FileNotFoundException; 5 | import java.io.InputStream; 6 | 7 | public class NormalFileResolver implements IFileResolver { 8 | private final String root; 9 | 10 | public NormalFileResolver(String root) { 11 | if (root.isEmpty() || root.equals("/")) this.root = ""; 12 | else { 13 | if (root.startsWith("/")) root = root.substring(1); 14 | if (!root.endsWith("/")) root = root + "/"; 15 | 16 | this.root = root; 17 | } 18 | } 19 | 20 | @Override 21 | public String resolvePath(String path) { 22 | return root + (path.startsWith("/") ? path.substring(1) : path); 23 | } 24 | 25 | @Override 26 | public InputStream get(String path) { 27 | try { 28 | return new FileInputStream(resolvePath(path)); 29 | } catch (FileNotFoundException e) { 30 | return null; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/theme/fileresolvers/ResourceFileResolver.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.theme.fileresolvers; 2 | 3 | import java.io.InputStream; 4 | 5 | public class ResourceFileResolver implements IFileResolver { 6 | private final String root; 7 | 8 | public ResourceFileResolver(String root) { 9 | if (!root.startsWith("/")) root = "/" + root; 10 | if (!root.endsWith("/")) root += "/"; 11 | 12 | this.root = root; 13 | } 14 | 15 | @Override 16 | public String resolvePath(String path) { 17 | return root + (path.startsWith("/") ? path.substring(1) : path); 18 | } 19 | 20 | @Override 21 | public InputStream get(String path) { 22 | return ResourceFileResolver.class.getResourceAsStream(resolvePath(path)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/theme/parser/ParseException.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.theme.parser; 2 | 3 | public class ParseException extends RuntimeException { 4 | public ParseException(String format, Object... args) { 5 | super(String.format(format, args)); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/utils/CharFilters.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.utils; 2 | 3 | public class CharFilters { 4 | public static final ICharFilter ALL = (text, c, cursor) -> true; 5 | 6 | public static final ICharFilter INTEGER = (text, c, cursor) -> { 7 | if (c == '-') return text.isEmpty() || (cursor == 0 && !text.contains("-")); 8 | return c >= '0' && c <= '9'; 9 | }; 10 | 11 | public static final ICharFilter DOUBLE = (text, c, cursor) -> { 12 | if (c == '-') return text.isEmpty() || (cursor == 0 && !text.contains("-")); 13 | if (c == '.') return !text.contains(".") && (!text.contains("-") || cursor > 0); 14 | return c >= '0' && c <= '9'; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/utils/ICharFilter.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.utils; 2 | 3 | public interface ICharFilter { 4 | boolean filter(String text, char c, int cursor); 5 | } 6 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/utils/IntStack.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.utils; 2 | 3 | public class IntStack { 4 | private int[] array; 5 | private int size; 6 | 7 | public IntStack(int defaultCapacity) { 8 | array = new int[defaultCapacity]; 9 | } 10 | 11 | public void push(int value) { 12 | if (size >= array.length) grow(); 13 | array[size++] = value; 14 | } 15 | 16 | public int pop() { 17 | return array[--size]; 18 | } 19 | 20 | public int peek() { 21 | return array[size - 1]; 22 | } 23 | 24 | public int size() { 25 | return size; 26 | } 27 | 28 | public boolean isEmpty() { 29 | return size == 0; 30 | } 31 | 32 | private void grow() { 33 | int capacity = Math.max(array.length + 1, (int) (array.length * 1.75)); 34 | 35 | int[] newArray = new int[capacity]; 36 | System.arraycopy(array, 0, newArray, 0, size); 37 | 38 | array = newArray; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/utils/Lists.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.utils; 2 | 3 | import java.util.AbstractList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | public class Lists { 8 | @SuppressWarnings("unchecked") 9 | public static List of() { 10 | return Collections.EMPTY_LIST; 11 | } 12 | 13 | public static List of(T element) { 14 | return new SingleImmutableList<>(element); 15 | } 16 | 17 | private static class SingleImmutableList extends AbstractList { 18 | private final T element; 19 | 20 | public SingleImmutableList(T element) { 21 | this.element = element; 22 | } 23 | 24 | @Override 25 | public T get(int index) { 26 | return element; 27 | } 28 | 29 | @Override 30 | public int size() { 31 | return 1; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/utils/Matrix.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.utils; 2 | 3 | import java.nio.FloatBuffer; 4 | 5 | import static org.lwjgl.system.MemoryUtil.memSet; 6 | 7 | // Code adapted from JOML 8 | public class Matrix { 9 | public static void identity(FloatBuffer buffer) { 10 | memSet(buffer, 0); 11 | 12 | buffer.put(0, 1); 13 | buffer.put(5, 1); 14 | buffer.put(10, 1); 15 | buffer.put(15, 1); 16 | } 17 | 18 | public static FloatBuffer ortho(FloatBuffer buffer, float left, float right, float bottom, float top, float zNear, float zFar) { 19 | identity(buffer); 20 | 21 | buffer.put(0, 2.0f / (right - left)); 22 | buffer.put(5, 2.0f / (top - bottom)); 23 | buffer.put(10, zNear - zFar); 24 | buffer.put(12, (right + left) / (left - right)); 25 | buffer.put(13, (top + bottom) / (bottom - top)); 26 | buffer.put(14, (zFar + zNear) / (zNear - zFar)); 27 | 28 | return buffer; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.utils; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Arrays; 8 | 9 | public class Utils { 10 | public static boolean IS_MAC = System.getProperty("os.name").contains("Mac"); 11 | 12 | public static byte[] read(InputStream in) { 13 | ByteArrayOutputStream result = new ByteArrayOutputStream(); 14 | byte[] bytes = new byte[1024]; 15 | 16 | try { 17 | for (int count; (count = in.read(bytes)) != -1;) { 18 | result.write(bytes, 0, count); 19 | } 20 | 21 | } catch (IOException e) { 22 | e.printStackTrace(); 23 | } finally { 24 | try { 25 | in.close(); 26 | } catch (IOException e) { 27 | e.printStackTrace(); 28 | } 29 | } 30 | 31 | return result.toByteArray(); 32 | } 33 | 34 | public static byte[] readResource(String path) { 35 | InputStream in = Utils.class.getResourceAsStream(path); 36 | if (in == null) return new byte[0]; 37 | 38 | return read(in); 39 | } 40 | 41 | public static String readResourceString(String path) { 42 | return new String(readResource(path), StandardCharsets.UTF_8); 43 | } 44 | 45 | public static T[] combine(T[] a, T... b) { 46 | T[] result = Arrays.copyOf(a, a.length + b.length); 47 | System.arraycopy(b, 0, result, a.length, b.length); 48 | return result; 49 | } 50 | 51 | public static int clamp(int value, int min, int max) { 52 | if (value < min) return min; 53 | return Math.min(value, max); 54 | } 55 | 56 | public static double clamp(double value, double min, double max) { 57 | if (value < min) return min; 58 | return Math.min(value, max); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/Cell.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pts.properties.Properties; 4 | import org.meteordev.pts.utils.AlignX; 5 | import org.meteordev.pts.utils.AlignY; 6 | 7 | /** Wrapper for a widget that is placed inside another widget. */ 8 | public class Cell { 9 | private final T widget; 10 | 11 | public int x, y; 12 | public int width, height; 13 | 14 | public boolean expandCellX; 15 | private boolean expandWidgetX; 16 | 17 | public Cell(T widget) { 18 | this.widget = widget; 19 | 20 | if (widget instanceof WHorizontalSeparator) expandX(); 21 | } 22 | 23 | /** @return the widget this cell wraps. */ 24 | public T widget() { 25 | return widget; 26 | } 27 | 28 | /** Makes it so this cell will try to take all available width. If multiple cells in the same row try to do so then the available width will be equally split. */ 29 | public Cell expandCellX() { 30 | expandCellX = true; 31 | return this; 32 | } 33 | 34 | /** Same as {@link #expandCellX()} but also expands the widget to the same width as this cell. */ 35 | public Cell expandX() { 36 | expandCellX = expandWidgetX = true; 37 | return this; 38 | } 39 | 40 | /** Adds or removes specified tag based on if this widget already contains the tag. */ 41 | public Cell tag(String tag) { 42 | widget.tag(tag); 43 | return this; 44 | } 45 | 46 | /** Adds or removes specified tag based on if this widget already contains the tag. */ 47 | public Cell tag(String tag, boolean shouldHave) { 48 | widget.tag(tag, shouldHave); 49 | return this; 50 | } 51 | 52 | /** Aligns the widget to the bounds of this cell. */ 53 | public void align() { 54 | if (expandWidgetX) widget.width = width; 55 | 56 | switch (widget.get(Properties.ALIGN_X)) { 57 | case Left -> widget.x = x; 58 | case Center -> widget.x = x + width / 2 - widget.width / 2; 59 | case Right -> widget.x = x + width - widget.width; 60 | } 61 | 62 | switch (widget.get(Properties.ALIGN_Y)) { 63 | case Bottom -> widget.y = y + height - widget.height; 64 | case Center -> widget.y = y + height / 2 - widget.height / 2; 65 | case Top -> widget.y = y; 66 | } 67 | } 68 | 69 | // Alignment 70 | 71 | /** Sets {@link Properties#ALIGN_X} to {@link AlignX#Left} for this widget. */ 72 | public Cell left() { 73 | widget.set(Properties.ALIGN_X, AlignX.Left); 74 | return this; 75 | } 76 | 77 | /** Sets {@link Properties#ALIGN_X} to {@link AlignX#Center} for this widget. */ 78 | public Cell centerX() { 79 | widget.set(Properties.ALIGN_X, AlignX.Center); 80 | return this; 81 | } 82 | 83 | /** Sets {@link Properties#ALIGN_X} to {@link AlignX#Right} for this widget. */ 84 | public Cell right() { 85 | widget.set(Properties.ALIGN_X, AlignX.Right); 86 | return this; 87 | } 88 | 89 | /** Sets {@link Properties#ALIGN_Y} to {@link AlignY#Bottom} for this widget. */ 90 | public Cell bottom() { 91 | widget.set(Properties.ALIGN_Y, AlignY.Bottom); 92 | return this; 93 | } 94 | 95 | /** Sets {@link Properties#ALIGN_Y} to {@link AlignY#Center} for this widget. */ 96 | public Cell centerY() { 97 | widget.set(Properties.ALIGN_Y, AlignY.Center); 98 | return this; 99 | } 100 | 101 | /** Sets {@link Properties#ALIGN_Y} to {@link AlignY#Top} for this widget. */ 102 | public Cell top() { 103 | widget.set(Properties.ALIGN_Y, AlignY.Top); 104 | return this; 105 | } 106 | 107 | /** Same as calling {@link #centerX()} and {@link #centerY()}. */ 108 | public Cell center() { 109 | centerX(); 110 | return centerY(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WButton.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pulsar.input.MouseMovedEvent; 4 | import org.meteordev.pulsar.layout.HorizontalLayout; 5 | import org.meteordev.pts.properties.Properties; 6 | 7 | import static org.meteordev.pulsar.utils.Utils.combine; 8 | 9 | /** Button widget which can contain text, icon or both. */ 10 | public class WButton extends WPressable { 11 | protected static final String[] NAMES = combine(Widget.NAMES, "button"); 12 | 13 | public Runnable action; 14 | 15 | private WText textW; 16 | private WIcon iconW; 17 | 18 | public WButton(String text) { 19 | checkIcon(); 20 | 21 | if (text != null) { 22 | this.textW = add(new WButtonText(text)).expandCellX().widget(); 23 | } 24 | } 25 | 26 | // TODO: Bad 27 | public void checkIcon() { 28 | if (iconW != null) { 29 | remove(iconW); 30 | iconW = null; 31 | } 32 | 33 | String icon = get(Properties.ICON); 34 | if (icon != null && !icon.equals("none")) { 35 | iconW = new WButtonIcon(); 36 | iconW.tag(icon); 37 | 38 | cells.add(0, create(iconW)); 39 | layout = new HorizontalLayout(); 40 | } 41 | } 42 | 43 | @Override 44 | public String[] names() { 45 | return NAMES; 46 | } 47 | 48 | @Override 49 | public void invalidStyle() { 50 | super.invalidStyle(); 51 | if (iconW != null) iconW.invalidStyle(); 52 | } 53 | 54 | @Override 55 | protected void doAction() { 56 | if (action != null) action.run(); 57 | } 58 | 59 | @Override 60 | public Widget tag(String tag) { 61 | if (textW != null) textW.tag(tag); 62 | if (iconW != null) iconW.tag(tag, textW == null || textW.hasTag(tag)); 63 | 64 | return super.tag(tag); 65 | } 66 | 67 | @Override 68 | public Widget tag(String tag, boolean shouldHave) { 69 | if (textW != null) textW.tag(tag, shouldHave); 70 | if (iconW != null) iconW.tag(tag, shouldHave); 71 | 72 | return super.tag(tag, shouldHave); 73 | } 74 | 75 | public void setText(String text) { 76 | if (textW == null) textW = add(new WText(text)).widget(); 77 | else textW.setText(text); 78 | } 79 | 80 | protected class WButtonIcon extends WIcon { 81 | protected static final String[] NAMES = combine(WIcon.NAMES, "button-icon"); 82 | 83 | @Override 84 | public String[] names() { 85 | return NAMES; 86 | } 87 | 88 | @Override 89 | protected void detectHovered(MouseMovedEvent event) {} 90 | 91 | @Override 92 | public boolean isHovered() { 93 | return WButton.this.isHovered(); 94 | } 95 | 96 | @Override 97 | public boolean isPressed() { 98 | return WButton.this.isPressed(); 99 | } 100 | } 101 | 102 | protected static class WButtonText extends WText { 103 | protected static final String[] NAMES = combine(WText.NAMES, "button-text"); 104 | 105 | public WButtonText(String text) { 106 | super(text); 107 | } 108 | 109 | @Override 110 | public String[] names() { 111 | return NAMES; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WCheckbox.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pts.properties.Properties; 4 | import org.meteordev.pts.utils.Color4; 5 | import org.meteordev.pulsar.rendering.Renderer; 6 | 7 | import static org.meteordev.pulsar.utils.Utils.combine; 8 | 9 | public class WCheckbox extends WPressable { 10 | protected static final String[] NAMES = combine(Widget.NAMES, "checkbox"); 11 | 12 | public Runnable action; 13 | public boolean checked; 14 | 15 | private final Widget inner; 16 | 17 | public WCheckbox(boolean checked) { 18 | this.checked = checked; 19 | 20 | inner = add(new WInner()).widget(); 21 | } 22 | 23 | @Override 24 | public String[] names() { 25 | return NAMES; 26 | } 27 | 28 | @Override 29 | public void invalidStyle() { 30 | super.invalidStyle(); 31 | inner.invalidStyle(); 32 | } 33 | 34 | @Override 35 | protected void doAction() { 36 | checked = !checked; 37 | if (action != null) action.run(); 38 | } 39 | 40 | protected class WInner extends Widget { 41 | protected static final String[] NAMES = combine(Widget.NAMES, "checkbox-inner"); 42 | protected static final String[] ICON_NAMES = combine(WIcon.NAMES, "checkbox-inner"); 43 | 44 | private final boolean isIcon; 45 | 46 | public WInner() { 47 | isIcon = get(Properties.ICON_PATH) != null; 48 | } 49 | 50 | @Override 51 | public String[] names() { 52 | if (isIcon) return ICON_NAMES; 53 | return NAMES; 54 | } 55 | 56 | @Override 57 | public void calculateSize() { 58 | super.calculateSize(); 59 | 60 | if (isIcon) width = height = Math.max(width, height); 61 | } 62 | 63 | @Override 64 | public boolean isHovered() { 65 | return WCheckbox.this.isHovered(); 66 | } 67 | 68 | @Override 69 | public boolean isPressed() { 70 | return WCheckbox.this.isPressed(); 71 | } 72 | 73 | @Override 74 | public void render(Renderer renderer, double delta) { 75 | if (checked) { 76 | if (isIcon) { 77 | String path = get(Properties.ICON_PATH); 78 | Color4 color = get(Properties.COLOR); 79 | 80 | if (path != null && color != null) renderer.icon(x, y, path, width, color); 81 | } 82 | else super.render(renderer, delta); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WDoubleEdit.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pulsar.utils.CharFilters; 4 | import org.meteordev.pulsar.utils.Utils; 5 | 6 | import java.text.DecimalFormat; 7 | 8 | import static org.meteordev.pulsar.utils.Utils.combine; 9 | 10 | public class WDoubleEdit extends WHorizontalList { 11 | protected static final String[] NAMES = combine(WHorizontalList.NAMES, "double-edit"); 12 | protected static DecimalFormat df = new DecimalFormat(); 13 | 14 | public Runnable action, actionOnUnfocused; 15 | public int decimalPlaces = 2; 16 | 17 | protected double value, min, max; 18 | 19 | protected WTextBox textBoxW; 20 | protected WSlider sliderW; 21 | 22 | public WDoubleEdit(double value, Double min, Double max, double sliderMin, double sliderMax) { 23 | this.value = value; 24 | this.min = min != null ? min : Double.MIN_VALUE; 25 | this.max = max != null ? max : Double.MAX_VALUE; 26 | 27 | init(true, sliderMin, sliderMax); 28 | } 29 | 30 | public WDoubleEdit(double value, Double min, Double max) { 31 | this.value = value; 32 | this.min = min != null ? min : Double.MIN_VALUE; 33 | this.max = max != null ? max : Double.MAX_VALUE; 34 | 35 | init(false, 0, 0); 36 | } 37 | 38 | private void init(boolean slider, double sliderMin, double sliderMax) { 39 | textBoxW = add(new WTextBox(format())).widget(); 40 | textBoxW.tag("double-edit"); 41 | textBoxW.filter = CharFilters.DOUBLE; 42 | 43 | textBoxW.actionOnUnfocused = () -> { 44 | double v = 0; 45 | 46 | try { 47 | v = Double.parseDouble(textBoxW.get()); 48 | } 49 | catch (NumberFormatException ignored) {} 50 | 51 | double prev = value; 52 | set(v); 53 | 54 | if (prev != value) runActions(); 55 | }; 56 | 57 | if (slider) { 58 | if (sliderMin < min) sliderMin = min; 59 | if (sliderMax > max) sliderMax = max; 60 | 61 | sliderW = add(new WSlider(value, sliderMin, sliderMax)).expandX().widget(); 62 | sliderW.tag("double-edit"); 63 | 64 | sliderW.action = () -> { 65 | double prev = value; 66 | value = Utils.clamp(sliderW.get(), min, max); 67 | 68 | textBoxW.set(format()); 69 | 70 | if (prev != value && action != null) action.run(); 71 | }; 72 | 73 | sliderW.actionOnRelease = () -> { 74 | if (actionOnUnfocused != null) actionOnUnfocused.run(); 75 | }; 76 | } 77 | else { 78 | WButton minus = add(new WButton("-")).widget(); 79 | minus.tag("double-edit"); 80 | minus.action = () -> buttonAdd(-1); 81 | minus.checkIcon(); 82 | 83 | WButton plus = add(new WButton("+")).widget(); 84 | plus.tag("double-edit"); 85 | plus.action = () -> buttonAdd(1); 86 | plus.checkIcon(); 87 | } 88 | } 89 | 90 | @Override 91 | public String[] names() { 92 | return NAMES; 93 | } 94 | 95 | private void runActions() { 96 | if (action != null) action.run(); 97 | if (actionOnUnfocused != null) actionOnUnfocused.run(); 98 | } 99 | 100 | private void buttonAdd(int delta) { 101 | double prev = value; 102 | set(value + delta); 103 | 104 | if (prev != value) runActions(); 105 | } 106 | 107 | public double get() { 108 | return value; 109 | } 110 | 111 | public void set(double value) { 112 | this.value = Utils.clamp(value, min, max); 113 | 114 | textBoxW.set(format()); 115 | if (sliderW != null) sliderW.set(value); 116 | } 117 | 118 | protected String format() { 119 | df.setMaximumFractionDigits(decimalPlaces); 120 | return df.format(value); 121 | } 122 | 123 | static { 124 | df.setMinimumFractionDigits(0); 125 | df.setGroupingUsed(false); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WHorizontalList.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pulsar.layout.HorizontalLayout; 4 | 5 | import static org.meteordev.pulsar.utils.Utils.combine; 6 | 7 | /** Horizontal list container widget which uses {@link HorizontalLayout}. */ 8 | public class WHorizontalList extends WContainer { 9 | protected static final String[] SELF_NON_SCROLL_MODE_NAMES = combine(WContainer.SELF_NON_SCROLL_MODE_NAMES, "horizontal-list"); 10 | protected static final String[] CONTENTS_NON_SCROLL_MODE_NAMES = combine(WContainer.CONTENTS_NON_SCROLL_MODE_NAMES, "horizontal-list"); 11 | 12 | public WHorizontalList() { 13 | layout = new HorizontalLayout(); 14 | } 15 | 16 | @Override 17 | protected String[] getSelfNonScrollModeNames() { 18 | return SELF_NON_SCROLL_MODE_NAMES; 19 | } 20 | 21 | @Override 22 | protected String[] getContentsScrollModeNames() { 23 | return CONTENTS_NON_SCROLL_MODE_NAMES; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WHorizontalSeparator.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pts.properties.Properties; 4 | import org.meteordev.pts.utils.Color4; 5 | import org.meteordev.pts.utils.Vec2; 6 | import org.meteordev.pts.utils.Vec4; 7 | import org.meteordev.pulsar.rendering.Renderer; 8 | import org.meteordev.pulsar.utils.Utils; 9 | 10 | public class WHorizontalSeparator extends Widget { 11 | protected static final String[] NAMES = Utils.combine(Widget.NAMES, "horizontal-separator"); 12 | 13 | protected WText textW; 14 | 15 | public WHorizontalSeparator(String text) { 16 | if (text != null) { 17 | tag("has-text"); 18 | textW = add(new WHSText(text)).expandCellX().widget(); 19 | } 20 | } 21 | 22 | public WHorizontalSeparator() { 23 | this(null); 24 | } 25 | 26 | @Override 27 | public String[] names() { 28 | return NAMES; 29 | } 30 | 31 | @Override 32 | protected void onRender(Renderer renderer, double delta) { 33 | Vec4 radius = get(Properties.RADIUS); 34 | Color4 backgroundColor = get(Properties.BACKGROUND_COLOR); 35 | Vec2 size = get(Properties.SIZE); 36 | Vec2 spacing = get(Properties.SPACING); 37 | 38 | if (backgroundColor != null) { 39 | int y = this.y + height / 2 - size.intY() / 2 - 1; 40 | 41 | if (textW == null) { 42 | renderer.quad(x, y, width, size.intY(), radius, 0, backgroundColor, null); 43 | } 44 | else { 45 | int w = textW.x - x - spacing.intX(); 46 | if (w > 0) renderer.quad(x, y, w, size.intY(), radius, 0, backgroundColor, null); 47 | 48 | int x = textW.x + textW.width + spacing.intX(); 49 | w = (this.x + width) - x; 50 | if (w > 0) renderer.quad(x, y, w, size.intY(), radius, 0, backgroundColor, null); 51 | } 52 | } 53 | } 54 | 55 | protected static class WHSText extends WText { 56 | protected static final String[] NAMES = Utils.combine(WText.NAMES, "horizontal-separator-text"); 57 | 58 | public WHSText(String text) { 59 | super(text); 60 | } 61 | 62 | @Override 63 | public String[] names() { 64 | return NAMES; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WIcon.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pts.properties.Properties; 4 | import org.meteordev.pts.utils.Color4; 5 | import org.meteordev.pulsar.rendering.Renderer; 6 | 7 | import static org.meteordev.pulsar.utils.Utils.combine; 8 | 9 | public class WIcon extends Widget { 10 | protected static final String[] NAMES = combine(Widget.NAMES, "icon"); 11 | 12 | @Override 13 | public String[] names() { 14 | return NAMES; 15 | } 16 | 17 | @Override 18 | public void calculateSize() { 19 | super.calculateSize(); 20 | 21 | width = height = Math.max(width, height); 22 | } 23 | 24 | @Override 25 | protected void onRender(Renderer renderer, double delta) { 26 | String path = get(Properties.ICON_PATH); 27 | Color4 color = get(Properties.COLOR); 28 | 29 | if (path != null && color != null) renderer.icon(x, y, path, width, color); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WImage.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.juno.api.JunoProvider; 4 | import org.meteordev.juno.api.texture.Filter; 5 | import org.meteordev.juno.api.texture.Format; 6 | import org.meteordev.juno.api.texture.Texture; 7 | import org.meteordev.juno.api.texture.Wrap; 8 | import org.meteordev.pulsar.rendering.Renderer; 9 | 10 | import java.nio.ByteBuffer; 11 | 12 | import static org.meteordev.pulsar.utils.Utils.combine; 13 | 14 | public class WImage extends Widget { 15 | protected static final String[] NAMES = combine(Widget.NAMES, "image"); 16 | 17 | private final Texture texture; 18 | 19 | public WImage(Texture texture) { 20 | this.texture = texture; 21 | } 22 | 23 | public WImage(ByteBuffer data, int width, int height) { 24 | if (data == null || width == 0 || height == 0) { 25 | texture = null; 26 | return; 27 | } 28 | 29 | texture = JunoProvider.get().createTexture(width, height, Format.RGBA, Filter.NEAREST, Filter.NEAREST, Wrap.CLAMP_TO_BORDER); 30 | texture.write(data); 31 | } 32 | 33 | @Override 34 | public String[] names() { 35 | return NAMES; 36 | } 37 | 38 | @Override 39 | public void render(Renderer renderer, double delta) { 40 | renderer.texture(x, y, width, height, texture, null); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WIntEdit.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pulsar.utils.CharFilters; 4 | import org.meteordev.pulsar.utils.Utils; 5 | 6 | import static org.meteordev.pulsar.utils.Utils.combine; 7 | 8 | public class WIntEdit extends WHorizontalList { 9 | protected static final String[] NAMES = combine(WHorizontalList.NAMES, "int-edit"); 10 | 11 | public Runnable action, actionOnUnfocused; 12 | 13 | protected int value, min, max; 14 | 15 | protected WTextBox textBoxW; 16 | protected WSlider sliderW; 17 | 18 | public WIntEdit(int value, Integer min, Integer max, int sliderMin, int sliderMax) { 19 | this.value = value; 20 | this.min = min != null ? min : Integer.MIN_VALUE; 21 | this.max = max != null ? max : Integer.MAX_VALUE; 22 | 23 | init(true, sliderMin, sliderMax); 24 | } 25 | 26 | public WIntEdit(int value, Integer min, Integer max) { 27 | this.value = value; 28 | this.min = min != null ? min : Integer.MIN_VALUE; 29 | this.max = max != null ? max : Integer.MAX_VALUE; 30 | 31 | init(false, 0, 0); 32 | } 33 | 34 | private void init(boolean slider, int sliderMin, int sliderMax) { 35 | textBoxW = add(new WTextBox(Integer.toString(value))).widget(); 36 | textBoxW.tag("int-edit"); 37 | textBoxW.filter = CharFilters.INTEGER; 38 | 39 | textBoxW.actionOnUnfocused = () -> { 40 | int v = 0; 41 | 42 | try { 43 | v = Integer.parseInt(textBoxW.get()); 44 | } 45 | catch (NumberFormatException ignored) {} 46 | 47 | int prev = value; 48 | set(v); 49 | 50 | if (prev != value) runActions(); 51 | }; 52 | 53 | if (slider) { 54 | if (sliderMin < min) sliderMin = min; 55 | if (sliderMax > max) sliderMax = max; 56 | 57 | sliderW = add(new WSlider(value, sliderMin, sliderMax)).expandX().widget(); 58 | sliderW.tag("int-edit"); 59 | 60 | sliderW.action = () -> { 61 | int prev = value; 62 | value = Utils.clamp((int) Math.round(sliderW.get()), min, max); 63 | 64 | textBoxW.set(Integer.toString(value)); 65 | 66 | if (prev != value && action != null) action.run(); 67 | }; 68 | 69 | sliderW.actionOnRelease = () -> { 70 | if (actionOnUnfocused != null) actionOnUnfocused.run(); 71 | }; 72 | } 73 | else { 74 | WButton minus = add(new WButton("-")).widget(); 75 | minus.tag("int-edit"); 76 | minus.action = () -> buttonAdd(-1); 77 | minus.checkIcon(); 78 | 79 | WButton plus = add(new WButton("+")).widget(); 80 | plus.tag("int-edit"); 81 | plus.action = () -> buttonAdd(1); 82 | plus.checkIcon(); 83 | } 84 | } 85 | 86 | @Override 87 | public String[] names() { 88 | return NAMES; 89 | } 90 | 91 | private void runActions() { 92 | if (action != null) action.run(); 93 | if (actionOnUnfocused != null) actionOnUnfocused.run(); 94 | } 95 | 96 | private void buttonAdd(int delta) { 97 | int prev = value; 98 | set(value + delta); 99 | 100 | if (prev != value) runActions(); 101 | } 102 | 103 | public int get() { 104 | return value; 105 | } 106 | 107 | public void set(int value) { 108 | this.value = Utils.clamp(value, min, max); 109 | 110 | textBoxW.set(Integer.toString(this.value)); 111 | if (sliderW != null) sliderW.set(value); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WPressable.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pulsar.input.MouseButtonEvent; 4 | 5 | /** Base class for all widgets that can be pressed. */ 6 | public abstract class WPressable extends Widget { 7 | private boolean pressed; 8 | 9 | @Override 10 | protected void onMousePressed(MouseButtonEvent event) { 11 | if (isHovered() && !event.used) { 12 | pressed = true; 13 | invalidStyle(); 14 | 15 | event.use(); 16 | } 17 | } 18 | 19 | @Override 20 | protected void onMouseReleased(MouseButtonEvent event) { 21 | if (pressed) { 22 | doAction(); 23 | 24 | pressed = false; 25 | invalidStyle(); 26 | } 27 | } 28 | 29 | protected void doAction() {} 30 | 31 | @Override 32 | public boolean isPressed() { 33 | return pressed; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WSection.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pulsar.input.Event; 4 | import org.meteordev.pulsar.layout.Layout; 5 | import org.meteordev.pulsar.layout.VerticalLayout; 6 | import org.meteordev.pulsar.utils.Utils; 7 | 8 | public class WSection extends Widget { 9 | protected static final String[] NAMES = Utils.combine(Widget.NAMES, "section"); 10 | 11 | protected final Widget header; 12 | protected final Widget body; 13 | 14 | protected boolean expanded; 15 | protected double animation; 16 | 17 | public WSection(String title, boolean expanded) { 18 | this.expanded = expanded; 19 | this.animation = expanded ? 1 : 0; 20 | this.layout = VerticalLayout.INSTANCE; 21 | 22 | header = super.add(new WHorizontalList()).expandX().widget(); 23 | body = super.add(new WVerticalList()).expandX().widget(); 24 | 25 | header.add(new WHorizontalSeparator(title)).expandX(); 26 | } 27 | 28 | public WSection(String title) { 29 | this(title, true); 30 | } 31 | 32 | @Override 33 | public String[] names() { 34 | return NAMES; 35 | } 36 | 37 | public T setLayout(T layout) { 38 | body.layout = layout; 39 | return layout; 40 | } 41 | 42 | @Override 43 | public Cell add(T widget) { 44 | return body.add(widget); 45 | } 46 | 47 | @Override 48 | public boolean remove(Widget widget) { 49 | return body.remove(widget); 50 | } 51 | 52 | @Override 53 | public void clear() { 54 | body.clear(); 55 | } 56 | 57 | @Override 58 | public void dispatch(Event event) { 59 | header.dispatch(event); 60 | if (expanded) body.dispatch(event); 61 | 62 | dispatchToSelf(event); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WTable.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pulsar.layout.TableLayout; 4 | 5 | import static org.meteordev.pulsar.utils.Utils.combine; 6 | 7 | /** Table container widget which uses {@link TableLayout}. */ 8 | public class WTable extends WContainer { 9 | protected static final String[] SELF_NON_SCROLL_MODE_NAMES = combine(WContainer.SELF_NON_SCROLL_MODE_NAMES, "table"); 10 | protected static final String[] CONTENTS_NON_SCROLL_MODE_NAMES = combine(WContainer.CONTENTS_NON_SCROLL_MODE_NAMES, "table"); 11 | 12 | public WTable() { 13 | layout = new TableLayout(); 14 | } 15 | 16 | @Override 17 | protected String[] getSelfNonScrollModeNames() { 18 | return SELF_NON_SCROLL_MODE_NAMES; 19 | } 20 | 21 | @Override 22 | protected String[] getContentsScrollModeNames() { 23 | return CONTENTS_NON_SCROLL_MODE_NAMES; 24 | } 25 | 26 | public void row() { 27 | ((TableLayout) layout).row(); 28 | } 29 | 30 | public int rowI() { 31 | return ((TableLayout) layout).rowI; 32 | } 33 | 34 | public void removeRow(int i) { 35 | ((TableLayout) layout).removeRow(this, i); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WTexture.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.juno.api.texture.Texture; 4 | import org.meteordev.pts.properties.Properties; 5 | import org.meteordev.pts.utils.Color4; 6 | import org.meteordev.pts.utils.ColorFactory; 7 | import org.meteordev.pulsar.rendering.Renderer; 8 | 9 | import static org.meteordev.pulsar.utils.Utils.combine; 10 | 11 | public class WTexture extends Widget { 12 | private static final Color4 WHITE = new Color4(ColorFactory.create(255, 255, 255, 255)); 13 | 14 | protected static final String[] NAMES = combine(Widget.NAMES, "texture"); 15 | 16 | public final Texture texture; 17 | private final int textureWidth, textureHeight; 18 | 19 | public WTexture(Texture texture, int width, int height) { 20 | this.texture = texture; 21 | this.textureWidth = width; 22 | this.textureHeight = height; 23 | } 24 | 25 | @Override 26 | public String[] names() { 27 | return NAMES; 28 | } 29 | 30 | @Override 31 | public void calculateSize() { 32 | width = textureWidth; 33 | height = textureHeight; 34 | } 35 | 36 | @Override 37 | protected void onRender(Renderer renderer, double delta) { 38 | Color4 color = get(Properties.COLOR); 39 | if (color == null) color = WHITE; 40 | 41 | renderer.texture(x, y, width, height, texture, color); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WVerticalList.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pulsar.layout.VerticalLayout; 4 | 5 | import static org.meteordev.pulsar.utils.Utils.combine; 6 | 7 | /** Vertical list container widget which uses {@link VerticalLayout}. */ 8 | public class WVerticalList extends WContainer { 9 | protected static final String[] SELF_NON_SCROLL_MODE_NAMES = combine(WContainer.SELF_NON_SCROLL_MODE_NAMES, "vertical-list"); 10 | protected static final String[] CONTENTS_NON_SCROLL_MODE_NAMES = combine(WContainer.CONTENTS_NON_SCROLL_MODE_NAMES, "vertical-list"); 11 | 12 | public WVerticalList() { 13 | layout = VerticalLayout.INSTANCE; 14 | } 15 | 16 | @Override 17 | protected String[] getSelfNonScrollModeNames() { 18 | return SELF_NON_SCROLL_MODE_NAMES; 19 | } 20 | 21 | @Override 22 | protected String[] getContentsScrollModeNames() { 23 | return CONTENTS_NON_SCROLL_MODE_NAMES; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pulsar/src/main/java/org/meteordev/pulsar/widgets/WWindowManager.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.pulsar.widgets; 2 | 3 | import org.meteordev.pulsar.input.Event; 4 | import org.meteordev.pulsar.input.MouseButtonEvent; 5 | import org.meteordev.pulsar.input.MouseMovedEvent; 6 | import org.meteordev.pulsar.layout.HorizontalLayout; 7 | import org.meteordev.pulsar.rendering.Renderer; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import static org.meteordev.pulsar.utils.Utils.combine; 13 | 14 | public class WWindowManager extends Widget { 15 | protected static String[] NAMES = combine(Widget.NAMES, "window-manager"); 16 | 17 | private final List windows = new ArrayList<>(); 18 | 19 | public WWindowManager() { 20 | layout = new HorizontalLayout(); 21 | } 22 | 23 | @Override 24 | public String[] names() { 25 | return NAMES; 26 | } 27 | 28 | @Override 29 | public Cell add(T widget) { 30 | if (widget instanceof WWindow window) windows.add(window); 31 | return super.add(widget); 32 | } 33 | 34 | @Override 35 | public boolean remove(Widget widget) { 36 | if (widget instanceof WWindow window) windows.remove(window); 37 | return super.remove(widget); 38 | } 39 | 40 | @Override 41 | public void clear() { 42 | windows.clear(); 43 | super.clear(); 44 | } 45 | 46 | @Override 47 | public void dispatch(Event event) { 48 | // Dispatch to windows and reorder them accordingly 49 | if (event instanceof MouseMovedEvent) { 50 | for (WWindow window : windows) { 51 | window.dispatch(event); 52 | } 53 | } else { 54 | 55 | // Find hovered with lowest z index 56 | WWindow lastHoveredWindow = null; 57 | for (int i = windows.size() - 1; i >= 0; i--) { 58 | WWindow window = windows.get(i); 59 | if (window.isHovered()) { 60 | lastHoveredWindow = window; 61 | break; 62 | } 63 | } 64 | 65 | if (lastHoveredWindow != null) { 66 | // Dispatch single window 67 | lastHoveredWindow.dispatch(event); 68 | 69 | // If window with lowest z index isn't the highest, make it the highest 70 | if (event instanceof MouseButtonEvent) { 71 | if (windows.indexOf(lastHoveredWindow) != windows.size() - 1) { 72 | windows.remove(lastHoveredWindow); 73 | windows.add(lastHoveredWindow); 74 | } 75 | } 76 | } 77 | } 78 | 79 | // Dispatch to children which are not windows 80 | for (Cell cell : cells) { 81 | if (!(cell.widget() instanceof WWindow)) cell.widget().dispatch(event); 82 | } 83 | 84 | // Dispatch to self 85 | dispatchToSelf(event); 86 | } 87 | 88 | @Override 89 | public void render(Renderer renderer, double delta) { 90 | // Render self 91 | onRender(renderer, delta); 92 | 93 | // Render children which are not windows 94 | for (Cell cell : cells) { 95 | if (!(cell.widget() instanceof WWindow)) cell.widget().render(renderer, delta); 96 | } 97 | 98 | // Render windows 99 | for (WWindow window : windows) window.render(renderer, delta); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pulsar/src/main/resources/pulsar/shaders/basic.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | out vec4 fragColor; 4 | 5 | in vec4 v_Color; 6 | 7 | void main() { 8 | fragColor = v_Color; 9 | } 10 | -------------------------------------------------------------------------------- /pulsar/src/main/resources/pulsar/shaders/basic.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (location = 0) in vec4 pos; 4 | layout (location = 1) in vec4 color; 5 | 6 | uniform mat4 u_Proj; 7 | 8 | out vec4 v_Color; 9 | 10 | void main() { 11 | gl_Position = u_Proj * pos; 12 | 13 | v_Color = color; 14 | } 15 | -------------------------------------------------------------------------------- /pulsar/src/main/resources/pulsar/shaders/icon.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | out vec4 fragColor; 4 | 5 | uniform sampler2D u_Texture; 6 | 7 | in vec2 v_TexCoord; 8 | in vec4 v_Color; 9 | 10 | void main() { 11 | fragColor = vec4(v_Color.rgb, v_Color.a * texture(u_Texture, v_TexCoord).a); 12 | } 13 | -------------------------------------------------------------------------------- /pulsar/src/main/resources/pulsar/shaders/rectangles.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | out vec4 fragColor; 4 | 5 | in vec2 v_LocalPos; 6 | flat in vec2 v_Size; 7 | flat in float v_Pixel; 8 | flat in vec4 v_Radius; 9 | flat in uint v_Background; 10 | in vec4 v_BackgroundColor; 11 | in vec4 v_OutlineColor; 12 | flat in float v_OutlineSize; 13 | 14 | float sdRoundedBox(vec2 p, vec2 b, vec4 r) { 15 | r.xy = (p.x > 0.0) ? r.wy : r.yx; 16 | r.x = (p.y > 0.0) ? r.w : r.z; 17 | vec2 q = abs(p) - b + r.x; 18 | return min(max(q.x, q.y),0.0) + length(max(q, 0.0)) - r.x; 19 | } 20 | 21 | float opOnion(float s, float r) { 22 | return abs(s) - r; 23 | } 24 | 25 | void main() { 26 | float distance = sdRoundedBox(v_LocalPos, v_Size, v_Radius * v_Pixel); 27 | 28 | // Background 29 | if (v_Background != uint(0)) { 30 | float a = smoothstep(v_Pixel, -v_Pixel, distance); 31 | 32 | fragColor = vec4(v_BackgroundColor.rgb, v_BackgroundColor.a * a); 33 | } 34 | 35 | // Outline 36 | if (v_OutlineSize != 0.0) { 37 | float distance2 = opOnion(distance, v_OutlineSize * v_Pixel); 38 | distance = max(distance, distance2); 39 | 40 | float a = smoothstep(v_Pixel, -v_Pixel, distance); 41 | 42 | if (v_Background == uint(0)) fragColor = vec4(v_OutlineColor.rgb, a); 43 | else fragColor.rgb = mix(fragColor.rgb, v_OutlineColor.rgb, a); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pulsar/src/main/resources/pulsar/shaders/rectangles.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (location = 0) in vec4 pos; 4 | layout (location = 1) in vec2 localPos; 5 | layout (location = 2) in vec2 size; 6 | layout (location = 3) in vec4 radius; 7 | layout (location = 4) in uint background; 8 | layout (location = 5) in vec4 backgroundColor; 9 | layout (location = 6) in vec4 outlineColor; 10 | layout (location = 7) in float outlineSize; 11 | 12 | uniform mat4 u_Proj; 13 | 14 | out vec2 v_LocalPos; 15 | flat out vec2 v_Size; 16 | flat out float v_Pixel; 17 | flat out vec4 v_Radius; 18 | flat out uint v_Background; 19 | out vec4 v_BackgroundColor; 20 | out vec4 v_OutlineColor; 21 | flat out float v_OutlineSize; 22 | 23 | void main() { 24 | gl_Position = u_Proj * pos; 25 | 26 | float bigger = max(size.x, size.y); 27 | 28 | v_LocalPos = localPos - (size - bigger) / bigger; 29 | v_Size = size / bigger; 30 | v_Pixel = 1.0 / (bigger); 31 | v_Radius = radius * 2.0; 32 | v_Background = background; 33 | v_BackgroundColor = backgroundColor; 34 | v_OutlineColor = outlineColor; 35 | v_OutlineSize = outlineSize; 36 | } 37 | -------------------------------------------------------------------------------- /pulsar/src/main/resources/pulsar/shaders/text.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | out vec4 fragColor; 4 | 5 | uniform sampler2D u_Texture; 6 | 7 | in vec2 v_TexCoord; 8 | in vec4 v_Color; 9 | 10 | void main() { 11 | fragColor = vec4(1.0, 1.0, 1.0, texture(u_Texture, v_TexCoord).r) * v_Color; 12 | } 13 | -------------------------------------------------------------------------------- /pulsar/src/main/resources/pulsar/shaders/texture.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | out vec4 fragColor; 4 | 5 | uniform sampler2D u_Texture; 6 | 7 | in vec2 v_TexCoord; 8 | in vec4 v_Color; 9 | 10 | void main() { 11 | vec4 color = texture(u_Texture, v_TexCoord); 12 | fragColor = color * v_Color; 13 | } 14 | -------------------------------------------------------------------------------- /pulsar/src/main/resources/pulsar/shaders/texture.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (location = 0) in vec4 pos; 4 | layout (location = 1) in vec2 texCoords; 5 | layout (location = 2) in vec4 color; 6 | 7 | uniform mat4 u_Proj; 8 | 9 | out vec2 v_TexCoord; 10 | out vec4 v_Color; 11 | 12 | void main() { 13 | gl_Position = u_Proj * pos; 14 | 15 | v_TexCoord = texCoords; 16 | v_Color = color; 17 | } 18 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "pulsar" 2 | include("pts", "pulsar", "example", "pts-intellij") 3 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | - Rewrite icon handling 2 | - Standardize custom widget child widgets being a custom widget or a tag 3 | - Font property instead of a global font constant 4 | - Bold / italic / underlined text 5 | - Box shadow 6 | - Blurred shadows 7 | - Different unit types (px, rem, %, ...) 8 | - Improve PTS VS Code extension 9 | - Transition property 10 | - Text transform property (none, uppercase, lowercase, capitalize) 11 | - Letter spacing property 12 | - Word spacing 13 | - Line height property 14 | - Transform property 15 | - Advanced PTS selectors (nesting, nth-child(x), first-child, last-child) 16 | - Multiple PTS selectors per style 17 | - Outline size per side 18 | - Gradients 19 | - Exported variables changeable at runtime 20 | - Cascaded properties --------------------------------------------------------------------------------