diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..ab16cc2 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,14 @@ +[target.x86_64-apple-darwin] +rustflags = [ + "-C", "link-arg=-Wl,-rpath,/usr/local/lib", + "-C", "link-arg=-Wl,-rpath,/opt/homebrew/lib", + "-C", "link-arg=-Wl,-rpath,/Users/thoq/VulkanSDK/1.4.341.0/macOS/lib" +] + +[target.aarch64-apple-darwin] +rustflags = [ + "-C", "link-arg=-Wl,-rpath,/usr/local/lib", + "-C", "link-arg=-Wl,-rpath,/opt/homebrew/lib", + "-C", "link-arg=-Wl,-rpath,/Users/thoq/VulkanSDK/1.4.341.0/macOS/lib" +] + diff --git a/.gitignore b/.gitignore index 184df98..d6948e7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,9 @@ build/ .DS_Store + + +# Added by cargo + +/target +target/ diff --git a/CMakeLists.txt b/CMakeLists.txt index a28824e..cdbf584 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(LIB_NAME ${PROJECT_NAME}) set(EXE_NAME moltenUI_demo) set(ASSET_BIN_DIR "$/assets") +set(MOLTEN_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include") + find_package(Vulkan REQUIRED) find_package(glfw3 3.4 REQUIRED) find_package(Freetype REQUIRED) @@ -39,6 +41,46 @@ add_executable(${EXE_NAME} demo/main.cpp) target_link_libraries(${EXE_NAME} PRIVATE ${LIB_NAME}) target_include_directories(${EXE_NAME} PRIVATE ${FREETYPE_INCLUDE_DIRS}) +add_library(molten_wrapper STATIC src/wrapper.cpp) + +target_include_directories(molten_wrapper PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${MOLTEN_INCLUDE_DIR} + ${Vulkan_INCLUDE_DIRS} + ${FREETYPE_INCLUDE_DIRS} +) + +target_link_libraries(molten_wrapper PUBLIC + ${LIB_NAME} + glfw + Vulkan::Vulkan + Freetype::Freetype +) + +if(APPLE) + target_link_libraries(molten_wrapper PUBLIC + "-framework Cocoa" + "-framework IOKit" + "-framework CoreVideo" + "-framework QuartzCore" + "-framework Metal" + ) +elseif(UNIX AND NOT APPLE) + target_link_libraries(molten_wrapper PUBLIC + dl + pthread + X11 + Xxf86vm + Xrandr + Xi + ) +endif() + +set_target_properties(molten_wrapper PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" +) + find_program(NAGA_EXECUTABLE naga) find_program(GLSLC_EXECUTABLE glslc) @@ -86,4 +128,5 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/assets ${ASSET_BIN_DIR} COMMENT "Copying assets to build directory..." -) \ No newline at end of file +) + diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..41e421d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,254 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "molten-ui" +version = "0.1.0" +dependencies = [ + "bindgen", + "cmake", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..898206f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "molten-ui" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[lib] +path = "src/lib.rs" + +[[bin]] +path = "src/main.rs" +name = "rsdemo" + +[build-dependencies] +bindgen = "0.72.1" +cmake = "0.1" + diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..8b2b473 --- /dev/null +++ b/build.rs @@ -0,0 +1,174 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-changed=src/wrapper.cpp"); + println!("cargo:rerun-if-changed=include/wrapper.hpp"); + println!("cargo:rerun-if-changed=CMakeLists.txt"); + println!("cargo:rerun-if-changed=shaders/*.wgsl"); + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let shaders_dir = manifest_dir.join("shaders"); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + let dxc_path = find_dxc(); + + let shaders = ["rect", "rounded_rect", "text"]; + let mut rust_code = + String::from("// Auto-generated SPIR-V shader data\n#[allow(dead_code)]\n\n"); + + for shader_name in &shaders { + let wgsl_path = shaders_dir.join(format!("{}.wgsl", shader_name)); + let spv_path = out_dir.join(format!("{}.spv", shader_name)); + + if wgsl_path.exists() { + if compile_wgsl_to_spirv(&dxc_path, &wgsl_path, &spv_path) { + if let Ok(spv_data) = fs::read(&spv_path) { + let var_name = format!("{}_spv", shader_name.replace("-", "_")); + rust_code.push_str(&format!( + "pub static {}: [u8; {}] = [\n", + var_name, + spv_data.len() + )); + + for (i, chunk) in spv_data.chunks(16).enumerate() { + rust_code.push_str(" "); + for (j, &byte) in chunk.iter().enumerate() { + if j > 0 { + rust_code.push_str(", "); + } + rust_code.push_str(&format!("0x{:02x}", byte)); + } + if i < (spv_data.len() + 15) / 16 - 1 { + rust_code.push_str(","); + } + rust_code.push_str("\n"); + } + rust_code.push_str("];\n\n"); + + println!( + "Compiled {} -> {} ({} bytes)", + wgsl_path.display(), + spv_path.display(), + spv_data.len() + ); + } + println!("cargo:rerun-if-changed={}", wgsl_path.display()); + } + } + } + + fs::write(out_dir.join("shaders.rs"), &rust_code).expect("Failed to write shaders.rs"); + + let dst = cmake::Config::new(".") + .build_target("molten_wrapper") + .build(); + + println!("cargo:rustc-link-search=native={}/build", dst.display()); + println!( + "cargo:rustc-link-search=native={}/build/_deps/fetch_vk_bootstrap-build", + dst.display() + ); + println!("cargo:rustc-link-lib=static=molten_wrapper"); + println!("cargo:rustc-link-lib=static=moltenUI"); + println!("cargo:rustc-link-lib=static=vk-bootstrap"); + + if cfg!(target_os = "macos") { + let home = env::var("HOME").unwrap(); + println!("cargo:rustc-link-search=native=/opt/homebrew/lib"); + println!("cargo:rustc-link-search=native=/usr/local/lib"); + println!( + "cargo:rustc-link-search=native={}/VulkanSDK/1.4.341.0/macOS/lib", + home + ); + + println!("cargo:rustc-link-lib=dylib=glfw"); + println!("cargo:rustc-link-lib=dylib=freetype"); + println!("cargo:rustc-link-lib=dylib=vulkan"); + println!("cargo:rustc-link-lib=framework=Cocoa"); + println!("cargo:rustc-link-lib=framework=IOKit"); + println!("cargo:rustc-link-lib=framework=CoreVideo"); + println!("cargo:rustc-link-lib=framework=QuartzCore"); + println!("cargo:rustc-link-lib=framework=Metal"); + println!("cargo:rustc-link-lib=c++"); + } else if cfg!(target_os = "linux") { + println!("cargo:rustc-link-lib=vulkan"); + println!("cargo:rustc-link-lib=glfw"); + println!("cargo:rustc-link-lib=freetype"); + println!("cargo:rustc-link-lib=stdc++"); + } + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let wrapper_header = manifest_dir.join("include/wrapper.hpp"); + + let bindings = bindgen::Builder::default() + .header(wrapper_header.to_str().unwrap()) + .clang_arg("-xc++") + .clang_arg("-std=c++17") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .allowlist_function("molten_.*") + .allowlist_type("MoltenColor") + .allowlist_type("MoltenVec2") + .allowlist_type("MoltenThemeColors") + .disable_header_comment() + .layout_tests(false) + .generate() + .expect("Unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} + +fn find_dxc() -> PathBuf { + let home = env::var("HOME").unwrap(); + let sdk_path = PathBuf::from(&home).join("VulkanSDK/1.4.341.0/macOS/bin/dxc"); + if sdk_path.exists() { + return sdk_path; + } + + if let Ok(path) = Command::new("which").arg("dxc").output() { + if path.status.success() { + let path_str = String::from_utf8_lossy(&path.stdout); + return PathBuf::from(path_str.trim()); + } + } + + PathBuf::from("/usr/local/bin/dxc") +} + +fn compile_wgsl_to_spirv(dxc_path: &PathBuf, wgsl_path: &PathBuf, spv_path: &PathBuf) -> bool { + let result = Command::new(dxc_path) + .args([ + wgsl_path.to_str().unwrap(), + "-spirv", + "-fentry-point=vs_main", + "-fentry-point=fs_main", + "-o", + spv_path.to_str().unwrap(), + ]) + .output(); + + match result { + Ok(output) => { + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!( + "Warning: Failed to compile {}: {}", + wgsl_path.display(), + stderr + ); + false + } else { + true + } + } + Err(e) => { + eprintln!("Warning: Could not run dxc: {}", e); + false + } + } +} diff --git a/demo/main.cpp b/demo/main.cpp index 8103681..9355009 100644 --- a/demo/main.cpp +++ b/demo/main.cpp @@ -1,3 +1,4 @@ +#include "molten/theme.hpp" #include #include #include @@ -81,7 +82,7 @@ void demo() { y += 45.0f; UI::label("moltenUI", {col3_x + 15, y}, theme.colors.accent); y += 24.0f; - UI::label_small("v1.0.0 - Vulkan UI", {col3_x + 15, y}, theme.colors.text_muted); + UI::label_small("v1.0.0 (Vulkan Backend)", {col3_x + 15, y}, theme.colors.text_muted); y += 24.0f; UI::separator({col3_x + 15, y}, col_w - 30); y += 20.0f; @@ -137,6 +138,7 @@ void demo() { int main() { try { + UI::Theme::set_theme(UI::Theme::create_purple_theme()); Molten app("MoltenUI Demo", 1280, 720); if(app.init() != 0) return 1; return app.run(demo); diff --git a/include/molten/theme.hpp b/include/molten/theme.hpp index c46d726..74f384b 100644 --- a/include/molten/theme.hpp +++ b/include/molten/theme.hpp @@ -40,7 +40,6 @@ void set_theme(const ThemeData &theme); ThemeData create_dark_theme(); ThemeData create_purple_theme(); -ThemeData create_light_theme(); } // namespace Theme } // namespace UI diff --git a/include/render.hpp b/include/render.hpp index ac41b99..c305020 100644 --- a/include/render.hpp +++ b/include/render.hpp @@ -3,11 +3,13 @@ #include "font.hpp" #include #include +#include namespace Render { extern std::unique_ptr fontRenderer; +void init_shaders_with_data(const std::vector &rectSpirv, const std::vector &roundedRectSpirv); void draw_frame(std::function callback); void draw_rect(float x, float y, float w, float h, glm::vec4 color); void draw_rounded_rect(float x, float y, float w, float h, float radius, glm::vec4 color); diff --git a/include/shader.hpp b/include/shader.hpp index 00732fd..56971af 100644 --- a/include/shader.hpp +++ b/include/shader.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include class Shader { @@ -11,6 +12,7 @@ class Shader { VkDescriptorSetLayout descSetLayout; Shader(const std::string &shaderPath); + Shader(const std::vector &spirvData); ~Shader(); void use(VkCommandBuffer cmd); diff --git a/include/wrapper.hpp b/include/wrapper.hpp new file mode 100644 index 0000000..3185e6a --- /dev/null +++ b/include/wrapper.hpp @@ -0,0 +1,73 @@ +#ifndef MOLTEN_WRAPPER_HPP +#define MOLTEN_WRAPPER_HPP + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void *MoltenApp; +typedef void *MoltenString; + +typedef struct { + float r, g, b, a; +} MoltenColor; + +typedef struct { + float x, y; +} MoltenVec2; + +MoltenApp molten_app_create(const char *title, int width, int height); +int molten_app_init(MoltenApp app); +int molten_app_run(MoltenApp app, void (*callback)(void)); +void molten_app_destroy(MoltenApp app); + +void molten_ui_clear_focus(); +void molten_ui_update(); +void molten_ui_draw_rect(float x, float y, float w, float h, MoltenColor color); +void molten_ui_draw_rounded_rect(float x, float y, float w, float h, float radius, MoltenColor color); + +void molten_ui_panel(MoltenVec2 pos, MoltenVec2 size, const char *title); +void molten_ui_label(const char *text, MoltenVec2 pos, MoltenColor color); +void molten_ui_label_small(const char *text, MoltenVec2 pos, MoltenColor color); +void molten_ui_separator(MoltenVec2 pos, float width); + +bool molten_ui_button(int id, MoltenVec2 pos, MoltenVec2 size, const char *label); +bool molten_ui_button_primary(int id, MoltenVec2 pos, MoltenVec2 size, const char *label); +bool molten_ui_checkbox(int id, bool *value, MoltenVec2 pos, const char *label); + +void molten_ui_slider_float(int id, float *value, float min, float max, MoltenVec2 pos, MoltenVec2 size); +void molten_ui_slider_int(int id, int *value, int min, int max, MoltenVec2 pos, MoltenVec2 size); +void molten_ui_progress_bar(float progress, MoltenVec2 pos, MoltenVec2 size, MoltenColor color); + +MoltenString molten_string_create(const char *str); +const char *molten_string_get(MoltenString s); +void molten_string_destroy(MoltenString s); +void molten_ui_input_field(int id, MoltenString str, MoltenVec2 pos, MoltenVec2 size); + +typedef struct { + MoltenColor background; + MoltenColor panel; + MoltenColor border; + MoltenColor text; + MoltenColor text_muted; + MoltenColor accent; + MoltenColor success; + MoltenColor error; + MoltenColor warning; +} MoltenThemeColors; + +void molten_theme_set_purple(); +void molten_theme_get_colors(MoltenThemeColors *colors); + +void molten_shaders_init_with_data(const uint32_t *rect_spirv, size_t rect_size, const uint32_t *rounded_rect_spirv, + size_t rounded_rect_size); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/shaders/rounded_rect.wgsl b/shaders/rounded_rect.wgsl index 336c931..bad5594 100644 --- a/shaders/rounded_rect.wgsl +++ b/shaders/rounded_rect.wgsl @@ -35,17 +35,17 @@ fn fs_main(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { let center = ubo.pos + ubo.size * 0.5; let half_size = ubo.size * 0.5; let p = uv - center; - let d = sd_rounded_box(p, half_size, ubo.radius); - let alpha = 1.0 - smoothstep(0.0, 1.0, d); - + var col = ubo.color; - + if ubo.border_width > 0.0 { let d_border = sd_rounded_box(p, half_size - vec2(ubo.border_width), ubo.radius - ubo.border_width); - let border_alpha = 1.0 - smoothstep(0.0, 1.0, d_border); - col = mix(ubo.border_color, ubo.color, border_alpha); + // border region: d < 0 (inside outer) AND d_border >= 0 (outside inner) + if (d < 0.0 && d_border >= 0.0) { + col = vec4(0.1, 0.1, 0.1, 1.0); + } } return vec4(col.rgb, col.a * alpha); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..cbc0d22 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,510 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +#[allow(non_upper_case_globals)] +#[allow(non_camel_case_types)] +#[allow(non_snake_case)] +#[allow(dead_code)] +mod ffi { + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + +use ffi::*; +use std::ffi::{CStr, CString}; +use std::ptr; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Color { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +impl Color { + pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self { + Self { r, g, b, a } + } + + pub const fn rgb(r: f32, g: f32, b: f32) -> Self { + Self::new(r, g, b, 1.0) + } + + fn to_molten_color(&self) -> MoltenColor { + MoltenColor { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } + + fn from_molten_color(c: MoltenColor) -> Self { + Self { + r: c.r, + g: c.g, + b: c.b, + a: c.a, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Vec2 { + pub x: f32, + pub y: f32, +} + +impl Vec2 { + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } + + fn to_molten_vec2(&self) -> MoltenVec2 { + MoltenVec2 { + x: self.x, + y: self.y, + } + } +} + +impl From<(f32, f32)> for Vec2 { + fn from((x, y): (f32, f32)) -> Self { + Self::new(x, y) + } +} + +#[derive(Debug, Clone)] +pub struct ThemeColors { + pub background: Color, + pub panel: Color, + pub border: Color, + pub text: Color, + pub text_muted: Color, + pub accent: Color, + pub success: Color, + pub error: Color, + pub warning: Color, +} + +impl ThemeColors { + pub fn get_current() -> Self { + unsafe { + let mut colors = MoltenThemeColors { + background: MoltenColor { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + panel: MoltenColor { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + border: MoltenColor { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + text: MoltenColor { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + text_muted: MoltenColor { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + accent: MoltenColor { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + success: MoltenColor { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + error: MoltenColor { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + warning: MoltenColor { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + }; + molten_theme_get_colors(&mut colors); + + Self { + background: Color::from_molten_color(colors.background), + panel: Color::from_molten_color(colors.panel), + border: Color::from_molten_color(colors.border), + text: Color::from_molten_color(colors.text), + text_muted: Color::from_molten_color(colors.text_muted), + accent: Color::from_molten_color(colors.accent), + success: Color::from_molten_color(colors.success), + error: Color::from_molten_color(colors.error), + warning: Color::from_molten_color(colors.warning), + } + } + } + + pub fn set_purple_theme() { + unsafe { + molten_theme_set_purple(); + } + } +} + +pub struct MoltenStr { + inner: MoltenString, +} + +impl MoltenStr { + pub fn new(s: &str) -> Self { + let c_str = CString::new(s).unwrap(); + unsafe { + Self { + inner: molten_string_create(c_str.as_ptr()), + } + } + } + + pub fn as_ptr(&self) -> MoltenString { + self.inner + } + + pub fn get(&self) -> &str { + unsafe { + let c_str = molten_string_get(self.inner); + CStr::from_ptr(c_str).to_str().unwrap_or("") + } + } +} + +impl Drop for MoltenStr { + fn drop(&mut self) { + unsafe { + molten_string_destroy(self.inner); + } + } +} + +pub struct App { + inner: MoltenApp, + owned: bool, +} + +impl App { + pub fn new(title: &str, width: i32, height: i32) -> Result { + let c_title = CString::new(title).map_err(|e| e.to_string())?; + + unsafe { + let app = molten_app_create(c_title.as_ptr(), width, height); + if app.is_null() { + Err("Failed to create Molten app".to_string()) + } else { + Ok(Self { + inner: app, + owned: true, + }) + } + } + } + + pub fn init(&self) -> Result<(), i32> { + unsafe { + let result = molten_app_init(self.inner); + if result == 0 { + Ok(()) + } else { + Err(result) + } + } + } + + pub fn run(&mut self, callback: F) -> i32 + where + F: FnMut() + 'static, + { + let callback_ptr = Box::into_raw(Box::new(callback)); + + unsafe { + static mut CALLBACK_PTR: *mut std::ffi::c_void = ptr::null_mut(); + CALLBACK_PTR = callback_ptr as *mut std::ffi::c_void; + + unsafe extern "C" fn callback_wrapper() + where + F: FnMut(), + { + unsafe { + if !CALLBACK_PTR.is_null() { + let callback = &mut *(CALLBACK_PTR as *mut F); + callback(); + } + } + } + + let result = molten_app_run(self.inner, Some(callback_wrapper::)); + + CALLBACK_PTR = ptr::null_mut(); + let _ = Box::from_raw(callback_ptr); + + result + } + } + + pub fn destroy(&mut self) { + if self.owned && !self.inner.is_null() { + unsafe { + molten_app_destroy(self.inner); + } + self.inner = ptr::null_mut(); + self.owned = false; + } + } +} + +impl Drop for App { + fn drop(&mut self) { + self.destroy(); + } +} + +pub mod ui { + use super::*; + + pub fn clear_focus() { + unsafe { + molten_ui_clear_focus(); + } + } + + pub fn update() { + unsafe { + molten_ui_update(); + } + } + + pub fn draw_rect(x: f32, y: f32, w: f32, h: f32, color: Color) { + unsafe { + molten_ui_draw_rect(x, y, w, h, color.to_molten_color()); + } + } + + pub fn draw_rounded_rect(x: f32, y: f32, w: f32, h: f32, radius: f32, color: Color) { + unsafe { + molten_ui_draw_rounded_rect(x, y, w, h, radius, color.to_molten_color()); + } + } + + pub fn panel(pos: impl Into, size: impl Into, title: &str) { + let pos = pos.into(); + let size = size.into(); + let c_title = CString::new(title).unwrap(); + + unsafe { + molten_ui_panel( + pos.to_molten_vec2(), + size.to_molten_vec2(), + c_title.as_ptr(), + ); + } + } + + pub fn label(text: &str, pos: impl Into, color: Color) { + let pos = pos.into(); + let c_text = CString::new(text).unwrap(); + + unsafe { + molten_ui_label( + c_text.as_ptr(), + pos.to_molten_vec2(), + color.to_molten_color(), + ); + } + } + + pub fn label_small(text: &str, pos: impl Into, color: Color) { + let pos = pos.into(); + let c_text = CString::new(text).unwrap(); + + unsafe { + molten_ui_label_small( + c_text.as_ptr(), + pos.to_molten_vec2(), + color.to_molten_color(), + ); + } + } + + pub fn separator(pos: impl Into, width: f32) { + let pos = pos.into(); + + unsafe { + molten_ui_separator(pos.to_molten_vec2(), width); + } + } + + pub fn button(id: i32, pos: impl Into, size: impl Into, label: &str) -> bool { + let pos = pos.into(); + let size = size.into(); + let c_label = CString::new(label).unwrap(); + + unsafe { + molten_ui_button( + id, + pos.to_molten_vec2(), + size.to_molten_vec2(), + c_label.as_ptr(), + ) + } + } + + pub fn button_primary( + id: i32, + pos: impl Into, + size: impl Into, + label: &str, + ) -> bool { + let pos = pos.into(); + let size = size.into(); + let c_label = CString::new(label).unwrap(); + + unsafe { + molten_ui_button_primary( + id, + pos.to_molten_vec2(), + size.to_molten_vec2(), + c_label.as_ptr(), + ) + } + } + + pub fn checkbox(id: i32, value: &mut bool, pos: impl Into, label: &str) -> bool { + let pos = pos.into(); + let c_label = CString::new(label).unwrap(); + + unsafe { + molten_ui_checkbox( + id, + value as *mut bool, + pos.to_molten_vec2(), + c_label.as_ptr(), + ) + } + } + + pub fn slider_float( + id: i32, + value: &mut f32, + min: f32, + max: f32, + pos: impl Into, + size: impl Into, + ) { + let pos = pos.into(); + let size = size.into(); + + unsafe { + molten_ui_slider_float( + id, + value as *mut f32, + min, + max, + pos.to_molten_vec2(), + size.to_molten_vec2(), + ); + } + } + + pub fn slider_int( + id: i32, + value: &mut i32, + min: i32, + max: i32, + pos: impl Into, + size: impl Into, + ) { + let pos = pos.into(); + let size = size.into(); + + unsafe { + molten_ui_slider_int( + id, + value as *mut i32, + min, + max, + pos.to_molten_vec2(), + size.to_molten_vec2(), + ); + } + } + + pub fn progress_bar(progress: f32, pos: impl Into, size: impl Into, color: Color) { + let pos = pos.into(); + let size = size.into(); + + unsafe { + molten_ui_progress_bar( + progress, + pos.to_molten_vec2(), + size.to_molten_vec2(), + color.to_molten_color(), + ); + } + } + + pub fn input_field(id: i32, string: &MoltenStr, pos: impl Into, size: impl Into) { + let pos = pos.into(); + let size = size.into(); + + unsafe { + molten_ui_input_field( + id, + string.as_ptr(), + pos.to_molten_vec2(), + size.to_molten_vec2(), + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_color() { + let color = Color::rgb(1.0, 0.5, 0.25); + assert_eq!(color.r, 1.0); + assert_eq!(color.g, 0.5); + assert_eq!(color.b, 0.25); + assert_eq!(color.a, 1.0); + } + + #[test] + fn test_vec2() { + let v = Vec2::new(10.0, 20.0); + assert_eq!(v.x, 10.0); + assert_eq!(v.y, 20.0); + + let v2: Vec2 = (30.0, 40.0).into(); + assert_eq!(v2.x, 30.0); + assert_eq!(v2.y, 40.0); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2e60006 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,256 @@ +use molten_ui::{ui, App, Color, MoltenStr, ThemeColors}; +use std::time::Instant; + +struct DemoState { + fps: f32, + frame_count: u32, + last_time: Instant, + email: MoltenStr, + password: MoltenStr, + remember_me: bool, + toggle1: bool, + toggle2: bool, + toggle3: bool, + volume: f32, + brightness: f32, + red: f32, + green: f32, + blue: f32, + count: i32, + switch: bool, +} + +impl DemoState { + fn new() -> Self { + Self { + fps: 60.0, + frame_count: 0, + last_time: Instant::now(), + email: MoltenStr::new(""), + password: MoltenStr::new(""), + remember_me: true, + toggle1: true, + toggle2: false, + toggle3: true, + volume: 75.0, + brightness: 50.0, + red: 200.0, + green: 50.0, + blue: 255.0, + count: 5, + switch: true, + } + } + + fn update_fps(&mut self) { + self.frame_count += 1; + let now = Instant::now(); + let elapsed = now.duration_since(self.last_time).as_secs_f32(); + + if elapsed >= 1.0 { + self.fps = self.frame_count as f32 / elapsed; + self.frame_count = 0; + self.last_time = now; + } + } +} + +fn main() { + ThemeColors::set_purple_theme(); + + let mut app = App::new("MoltenUI Demo", 1280, 720).expect("Failed to create app"); + + if let Err(code) = app.init() { + eprintln!("Failed to initialize app: {}", code); + return; + } + + let mut state = DemoState::new(); + + let exit_code = app.run(move || { + ui::clear_focus(); + let theme = ThemeColors::get_current(); + + ui::draw_rect(0.0, 0.0, 1280.0, 720.0, theme.background); + + state.update_fps(); + + const COL_W: f32 = 220.0; + const GAP: f32 = 20.0; + const COL1_X: f32 = 20.0; + const COL2_X: f32 = COL1_X + COL_W + GAP; + const COL3_X: f32 = COL2_X + COL_W + GAP; + const COL4_X: f32 = COL3_X + COL_W + GAP; + const START_Y: f32 = 20.0; + + let mut y = START_Y; + ui::panel((COL1_X, y), (COL_W, 230.0), "Sign In"); + y += 45.0; + ui::input_field(1, &state.email, (COL1_X + 15.0, y), (COL_W - 30.0, 36.0)); + y += 45.0; + ui::input_field(2, &state.password, (COL1_X + 15.0, y), (COL_W - 30.0, 36.0)); + y += 45.0; + ui::checkbox(3, &mut state.remember_me, (COL1_X + 15.0, y), "Remember me"); + y += 50.0; + ui::button_primary(4, (COL1_X + 15.0, y), (COL_W - 30.0, 36.0), "Sign In"); + + y = 250.0 + 25.0; + ui::panel((COL1_X, y), (COL_W, 140.0), "Toggles"); + y += 45.0; + ui::checkbox(10, &mut state.toggle1, (COL1_X + 15.0, y), "Enabled"); + y += 30.0; + ui::checkbox(11, &mut state.toggle2, (COL1_X + 15.0, y), "Notifications"); + y += 30.0; + ui::checkbox(12, &mut state.toggle3, (COL1_X + 15.0, y), "Auto-save"); + + y = START_Y; + ui::panel((COL2_X, y), (COL_W, 235.0), "Buttons"); + y += 45.0; + ui::button_primary(20, (COL2_X + 15.0, y), (COL_W - 30.0, 32.0), "Submit"); + y += 42.0; + ui::button(21, (COL2_X + 15.0, y), (COL_W - 30.0, 32.0), "Cancel"); + y += 42.0; + ui::button(22, (COL2_X + 15.0, y), (COL_W - 30.0, 32.0), "Delete"); + y += 42.0; + ui::button(23, (COL2_X + 15.0, y), (90.0, 28.0), "Edit"); + ui::button(24, (COL2_X + 115.0, y), (90.0, 28.0), "Copy"); + + y = 255.0 + 25.0; + ui::panel((COL2_X, y), (COL_W, 145.0), "Sliders"); + y += 45.0; + ui::label( + &format!("{}%", state.volume as i32), + (COL2_X + 15.0, y + 12.0), + theme.text_muted, + ); + ui::slider_float( + 14, + &mut state.volume, + 0.0, + 100.0, + (COL2_X + 55.0, y), + (COL_W - 75.0, 24.0), + ); + y += 45.0; + ui::slider_float( + 15, + &mut state.brightness, + 0.0, + 100.0, + (COL2_X + 15.0, y), + (COL_W - 30.0, 16.0), + ); + y += 28.0; + ui::progress_bar( + state.brightness / 100.0, + (COL2_X + 15.0, y), + (COL_W - 30.0, 10.0), + theme.accent, + ); + + y = START_Y; + ui::panel((COL3_X, y), (COL_W, 260.0), "Info"); + y += 45.0; + ui::label("moltenUI", (COL3_X + 15.0, y), theme.accent); + y += 24.0; + ui::label_small( + "v1.0.0 (Vulkan Backend)", + (COL3_X + 15.0, y), + theme.text_muted, + ); + y += 24.0; + ui::separator((COL3_X + 15.0, y), COL_W - 30.0); + y += 20.0; + ui::label_small("- Rounded corners", (COL3_X + 15.0, y), theme.text_muted); + y += 18.0; + ui::label_small("- Theme support", (COL3_X + 15.0, y), theme.text_muted); + y += 24.0; + ui::separator((COL3_X + 15.0, y), COL_W - 30.0); + y += 20.0; + ui::label( + &format!("FPS: {}", state.fps as i32), + (COL3_X + 15.0, y), + theme.text, + ); + + y = 280.0 + 25.0; + ui::panel((COL3_X, y), (COL_W, 165.0), "RGB Color"); + y += 45.0; + ui::slider_float( + 40, + &mut state.red, + 0.0, + 255.0, + (COL3_X + 15.0, y), + (COL_W - 30.0, 14.0), + ); + y += 22.0; + ui::slider_float( + 41, + &mut state.green, + 0.0, + 255.0, + (COL3_X + 15.0, y), + (COL_W - 30.0, 14.0), + ); + y += 22.0; + ui::slider_float( + 42, + &mut state.blue, + 0.0, + 255.0, + (COL3_X + 15.0, y), + (COL_W - 30.0, 14.0), + ); + y += 26.0; + ui::draw_rounded_rect( + COL3_X + 15.0, + y, + COL_W - 30.0, + 30.0, + 4.0, + Color::rgb(state.red / 255.0, state.green / 255.0, state.blue / 255.0), + ); + + y = START_Y; + ui::panel((COL4_X, y), (COL_W, 400.0), "All Widgets"); + y += 45.0; + ui::label("Labels:", (COL4_X + 15.0, y), theme.text); + y += 24.0; + ui::label_small("Small label", (COL4_X + 15.0, y), theme.text_muted); + y += 30.0; + ui::separator((COL4_X + 15.0, y), COL_W - 30.0); + y += 20.0; + ui::checkbox(60, &mut state.switch, (COL4_X + 15.0, y), "Switch"); + y += 35.0; + ui::separator((COL4_X + 15.0, y), COL_W - 30.0); + y += 20.0; + ui::slider_int( + 61, + &mut state.count, + 0, + 10, + (COL4_X + 15.0, y), + (COL_W - 30.0, 16.0), + ); + y += 45.0; + ui::label( + &format!("Count: {}", state.count), + (COL4_X + 15.0, y), + theme.text_muted, + ); + y += 35.0; + ui::separator((COL4_X + 15.0, y), COL_W - 30.0); + y += 20.0; + ui::draw_rounded_rect(COL4_X + 15.0, y, 50.0, 30.0, 6.0, theme.error); + ui::draw_rounded_rect(COL4_X + 75.0, y, 50.0, 30.0, 6.0, theme.success); + ui::draw_rounded_rect(COL4_X + 135.0, y, 50.0, 30.0, 6.0, theme.warning); + y += 45.0; + ui::draw_rect(COL4_X + 15.0, y, COL_W - 30.0, 15.0, theme.border); + + ui::update(); + }); + + app.destroy(); + std::process::exit(exit_code); +} diff --git a/src/render.cpp b/src/render.cpp index 42e0a45..12817d7 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -44,10 +44,112 @@ VkDescriptorPool roundedDescPool; VkDescriptorSet roundedDescriptorSets[MAX_FRAMES][MAX_RECTS_PER_FRAME]; int roundedRectCounters[MAX_FRAMES] = {0, 0, 0}; +bool shadersInitialized = false; + void init_resources() { + if(shadersInitialized) return; + rectShader = std::make_unique("shaders/rect"); roundedRectShader = std::make_unique("shaders/rounded_rect"); + fontRenderer = std::make_unique("../assets/fonts/GoogleSans-Regular.ttf", 14); + shadersInitialized = true; + + for(int frame = 0; frame < MAX_FRAMES; frame++) { + for(int i = 0; i < MAX_RECTS_PER_FRAME; i++) { + VkBufferCreateInfo bci{.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .size = sizeof(RectUBO), + .usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}; + VmaAllocationCreateInfo aci{.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT, + .usage = VMA_MEMORY_USAGE_AUTO}; + VmaAllocationInfo allocInfo; + vmaCreateBuffer(Init::allocator, &bci, &aci, &uniformBuffers[frame][i], &uniformAllocs[frame][i], + &allocInfo); + uniformMapped[frame][i] = allocInfo.pMappedData; + } + } + + VkDescriptorPoolSize poolSize{.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .descriptorCount = MAX_FRAMES * MAX_RECTS_PER_FRAME}; + VkDescriptorPoolCreateInfo pci{.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .maxSets = MAX_FRAMES * MAX_RECTS_PER_FRAME, + .poolSizeCount = 1, + .pPoolSizes = &poolSize}; + vkCreateDescriptorPool(Init::device, &pci, nullptr, &descPool); + + for(int frame = 0; frame < MAX_FRAMES; frame++) { + for(int i = 0; i < MAX_RECTS_PER_FRAME; i++) { + VkDescriptorSetLayout layout = rectShader->descSetLayout; + VkDescriptorSetAllocateInfo ai{.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + .descriptorPool = descPool, + .descriptorSetCount = 1, + .pSetLayouts = &layout}; + vkAllocateDescriptorSets(Init::device, &ai, &descriptorSets[frame][i]); + + VkDescriptorBufferInfo bufferInfo{ + .buffer = uniformBuffers[frame][i], .offset = 0, .range = sizeof(RectUBO)}; + VkWriteDescriptorSet write{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = descriptorSets[frame][i], + .dstBinding = 0, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .pBufferInfo = &bufferInfo}; + vkUpdateDescriptorSets(Init::device, 1, &write, 0, nullptr); + } + } + + for(int frame = 0; frame < MAX_FRAMES; frame++) { + for(int i = 0; i < MAX_RECTS_PER_FRAME; i++) { + VkBufferCreateInfo bci{.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .size = sizeof(RoundedRectUBO), + .usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}; + VmaAllocationCreateInfo aci{.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT, + .usage = VMA_MEMORY_USAGE_AUTO}; + VmaAllocationInfo allocInfo; + vmaCreateBuffer(Init::allocator, &bci, &aci, &roundedUniformBuffers[frame][i], + &roundedUniformAllocs[frame][i], &allocInfo); + roundedUniformMapped[frame][i] = allocInfo.pMappedData; + } + } + + VkDescriptorPoolSize roundedPoolSize{.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .descriptorCount = MAX_FRAMES * MAX_RECTS_PER_FRAME}; + VkDescriptorPoolCreateInfo roundedPci{.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .maxSets = MAX_FRAMES * MAX_RECTS_PER_FRAME, + .poolSizeCount = 1, + .pPoolSizes = &roundedPoolSize}; + vkCreateDescriptorPool(Init::device, &roundedPci, nullptr, &roundedDescPool); + + for(int frame = 0; frame < MAX_FRAMES; frame++) { + for(int i = 0; i < MAX_RECTS_PER_FRAME; i++) { + VkDescriptorSetLayout layout = roundedRectShader->descSetLayout; + VkDescriptorSetAllocateInfo ai{.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + .descriptorPool = roundedDescPool, + .descriptorSetCount = 1, + .pSetLayouts = &layout}; + vkAllocateDescriptorSets(Init::device, &ai, &roundedDescriptorSets[frame][i]); + + VkDescriptorBufferInfo bufferInfo{ + .buffer = roundedUniformBuffers[frame][i], .offset = 0, .range = sizeof(RoundedRectUBO)}; + VkWriteDescriptorSet write{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = roundedDescriptorSets[frame][i], + .dstBinding = 0, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .pBufferInfo = &bufferInfo}; + vkUpdateDescriptorSets(Init::device, 1, &write, 0, nullptr); + } + } +} + +void init_shaders_with_data(const std::vector &rectSpirv, const std::vector &roundedRectSpirv) { + if(shadersInitialized) return; + + rectShader = std::make_unique(rectSpirv); + roundedRectShader = std::make_unique(roundedRectSpirv); fontRenderer = std::make_unique("assets/fonts/GoogleSans-Regular.ttf", 14); + shadersInitialized = true; for(int frame = 0; frame < MAX_FRAMES; frame++) { for(int i = 0; i < MAX_RECTS_PER_FRAME; i++) { diff --git a/src/shader.cpp b/src/shader.cpp index 9b3beab..b4ccc0c 100644 --- a/src/shader.cpp +++ b/src/shader.cpp @@ -4,6 +4,28 @@ #include #include +static VkShaderModule createShaderModule(const std::vector &code) { + VkShaderModuleCreateInfo ci{.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + ci.codeSize = code.size() * sizeof(uint32_t); + ci.pCode = code.data(); + VkShaderModule mod; + if(vkCreateShaderModule(Init::device, &ci, nullptr, &mod) != VK_SUCCESS) { + throw std::runtime_error("Failed to create shader module"); + } + return mod; +} + +static void createPipelineStages(VkShaderModule shaderMod, VkPipelineShaderStageCreateInfo *stages) { + stages[0] = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_VERTEX_BIT, + .module = shaderMod, + .pName = "vs_main"}; + stages[1] = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_FRAGMENT_BIT, + .module = shaderMod, + .pName = "fs_main"}; +} + Shader::Shader(const std::string &shaderPath) { auto loadCode = [](const std::string &path) { std::ifstream file(path, std::ios::ate | std::ios::binary); @@ -19,28 +41,81 @@ Shader::Shader(const std::string &shaderPath) { return buffer; }; - auto createMod = [&](const std::vector &code) { - VkShaderModuleCreateInfo ci{.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - ci.codeSize = code.size() * sizeof(uint32_t); - ci.pCode = code.data(); - VkShaderModule mod; - if(vkCreateShaderModule(Init::device, &ci, nullptr, &mod) != VK_SUCCESS) { - throw std::runtime_error("Failed to create shader module"); - } - return mod; - }; - auto code = loadCode(shaderPath + ".spv"); - VkShaderModule shaderMod = createMod(code); - - VkPipelineShaderStageCreateInfo stages[2] = {{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - .stage = VK_SHADER_STAGE_VERTEX_BIT, - .module = shaderMod, - .pName = "vs_main"}, - {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - .stage = VK_SHADER_STAGE_FRAGMENT_BIT, - .module = shaderMod, - .pName = "fs_main"}}; + VkShaderModule shaderMod = createShaderModule(code); + + VkPipelineShaderStageCreateInfo stages[2]; + createPipelineStages(shaderMod, stages); + + VkDescriptorSetLayoutBinding uboBinding{.binding = 0, + .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT}; + + VkDescriptorSetLayoutCreateInfo descLayoutInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .bindingCount = 1, .pBindings = &uboBinding}; + vkCreateDescriptorSetLayout(Init::device, &descLayoutInfo, nullptr, &descSetLayout); + + VkPipelineLayoutCreateInfo lci{.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = 1, + .pSetLayouts = &descSetLayout, + .pushConstantRangeCount = 0, + .pPushConstantRanges = nullptr}; + vkCreatePipelineLayout(Init::device, &lci, nullptr, &layout); + + VkPipelineVertexInputStateCreateInfo vi{.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo ia{.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST}; + + VkDynamicState dynamicStates[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamicState{.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .dynamicStateCount = 2, + .pDynamicStates = dynamicStates}; + + VkPipelineViewportStateCreateInfo vps{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, .viewportCount = 1, .scissorCount = 1}; + VkPipelineRasterizationStateCreateInfo rs{.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .cullMode = VK_CULL_MODE_NONE, + .lineWidth = 1.0f}; + VkPipelineMultisampleStateCreateInfo ms{.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT}; + VkPipelineColorBlendAttachmentState cba{.blendEnable = VK_TRUE, + .srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA, + .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, + .colorBlendOp = VK_BLEND_OP_ADD, + .srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE, + .dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO, + .alphaBlendOp = VK_BLEND_OP_ADD, + .colorWriteMask = 0xF}; + VkPipelineColorBlendStateCreateInfo cbs{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, .attachmentCount = 1, .pAttachments = &cba}; + + VkGraphicsPipelineCreateInfo gpi{.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + .stageCount = 2, + .pStages = stages, + .pVertexInputState = &vi, + .pInputAssemblyState = &ia, + .pViewportState = &vps, + .pRasterizationState = &rs, + .pMultisampleState = &ms, + .pColorBlendState = &cbs, + .pDynamicState = &dynamicState, + .layout = layout, + .renderPass = Init::renderPass}; + + VkResult result = vkCreateGraphicsPipelines(Init::device, nullptr, 1, &gpi, nullptr, &pipeline); + if(result != VK_SUCCESS) { + throw std::runtime_error("Failed to create graphics pipeline!"); + } + + vkDestroyShaderModule(Init::device, shaderMod, nullptr); +} + +Shader::Shader(const std::vector &spirvData) { + VkShaderModule shaderMod = createShaderModule(spirvData); + + VkPipelineShaderStageCreateInfo stages[2]; + createPipelineStages(shaderMod, stages); VkDescriptorSetLayoutBinding uboBinding{.binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, diff --git a/src/theme.cpp b/src/theme.cpp index afd0f67..35e04cb 100644 --- a/src/theme.cpp +++ b/src/theme.cpp @@ -16,7 +16,7 @@ void set_theme(const ThemeData &theme) { ThemeData create_dark_theme() { ThemeData t; t.colors.background = {0.0f, 0.0f, 0.0f, 1.0f}; - t.colors.surface = {0.05f, 0.05f, 0.05f, 1.0f}; + t.colors.surface = {0.02f, 0.02f, 0.02f, 1.0f}; t.colors.surface_hover = {0.08f, 0.08f, 0.08f, 1.0f}; t.colors.surface_pressed = {0.03f, 0.03f, 0.03f, 1.0f}; t.colors.border = {0.15f, 0.15f, 0.15f, 1.0f}; diff --git a/src/wrapper.cpp b/src/wrapper.cpp new file mode 100644 index 0000000..d604db7 --- /dev/null +++ b/src/wrapper.cpp @@ -0,0 +1,161 @@ +#include "wrapper.hpp" +#include +#include +#include +#include + +extern "C" { + +MoltenApp molten_app_create(const char *title, int width, int height) { + try { + auto *app = new Molten(title, width, height); + return static_cast(app); + } catch(...) { + return nullptr; + } +} + +int molten_app_init(MoltenApp app) { + if(!app) return -1; + try { + auto *molten_app = static_cast(app); + return molten_app->init(); + } catch(...) { + return -1; + } +} + +static void (*g_callback)(void) = nullptr; + +static void callback_wrapper() { + if(g_callback) { + g_callback(); + } +} + +int molten_app_run(MoltenApp app, void (*callback)(void)) { + if(!app) return -1; + try { + g_callback = callback; + auto *molten_app = static_cast(app); + return molten_app->run(callback_wrapper); + } catch(...) { + return -1; + } +} + +void molten_app_destroy(MoltenApp app) { + if(app) { + delete static_cast(app); + } +} + +void molten_ui_clear_focus() { + UI::clear_focus(); +} + +void molten_ui_update() { + UI::update(); +} + +void molten_ui_draw_rect(float x, float y, float w, float h, MoltenColor color) { + UI::draw_rect(x, y, w, h, {color.r, color.g, color.b, color.a}); +} + +void molten_ui_draw_rounded_rect(float x, float y, float w, float h, float radius, MoltenColor color) { + UI::draw_rounded_rect(x, y, w, h, radius, {color.r, color.g, color.b, color.a}); +} + +void molten_ui_panel(MoltenVec2 pos, MoltenVec2 size, const char *title) { + UI::panel({pos.x, pos.y}, {size.x, size.y}, title); +} + +void molten_ui_label(const char *text, MoltenVec2 pos, MoltenColor color) { + UI::label(text, {pos.x, pos.y}, {color.r, color.g, color.b, color.a}); +} + +void molten_ui_label_small(const char *text, MoltenVec2 pos, MoltenColor color) { + UI::label_small(text, {pos.x, pos.y}, {color.r, color.g, color.b, color.a}); +} + +void molten_ui_separator(MoltenVec2 pos, float width) { + UI::separator({pos.x, pos.y}, width); +} + +bool molten_ui_button(int id, MoltenVec2 pos, MoltenVec2 size, const char *label) { + return UI::button(id, {pos.x, pos.y}, {size.x, size.y}, label); +} + +bool molten_ui_button_primary(int id, MoltenVec2 pos, MoltenVec2 size, const char *label) { + return UI::button_primary(id, {pos.x, pos.y}, {size.x, size.y}, label); +} + +bool molten_ui_checkbox(int id, bool *value, MoltenVec2 pos, const char *label) { + UI::checkbox(id, *value, {pos.x, pos.y}, label); + return *value; +} + +void molten_ui_slider_float(int id, float *value, float min, float max, MoltenVec2 pos, MoltenVec2 size) { + UI::slider_float(id, *value, min, max, {pos.x, pos.y}, {size.x, size.y}); +} + +void molten_ui_slider_int(int id, int *value, int min, int max, MoltenVec2 pos, MoltenVec2 size) { + UI::slider_int(id, *value, min, max, {pos.x, pos.y}, {size.x, size.y}); +} + +void molten_ui_progress_bar(float progress, MoltenVec2 pos, MoltenVec2 size, MoltenColor color) { + UI::progress_bar(progress, {pos.x, pos.y}, {size.x, size.y}, {color.r, color.g, color.b, color.a}); +} + +MoltenString molten_string_create(const char *str) { + return new std::string(str); +} + +const char *molten_string_get(MoltenString s) { + if(!s) return ""; + return static_cast(s)->c_str(); +} + +void molten_string_destroy(MoltenString s) { + if(s) { + delete static_cast(s); + } +} + +void molten_ui_input_field(int id, MoltenString str, MoltenVec2 pos, MoltenVec2 size) { + if(str) { + auto *cpp_str = static_cast(str); + UI::input_field(id, *cpp_str, {pos.x, pos.y}, {size.x, size.y}); + } +} + +void molten_theme_set_purple() { + UI::Theme::set_theme(UI::Theme::create_purple_theme()); +} + +void molten_theme_get_colors(MoltenThemeColors *colors) { + if(!colors) return; + + auto &theme = UI::Theme::get_current(); + colors->background = {theme.colors.background.r, theme.colors.background.g, theme.colors.background.b, + theme.colors.background.a}; + colors->panel = {theme.colors.background.r, theme.colors.background.g, theme.colors.background.b, + theme.colors.background.a}; + colors->border = {theme.colors.border.r, theme.colors.border.g, theme.colors.border.b, theme.colors.border.a}; + colors->text = {theme.colors.text.r, theme.colors.text.g, theme.colors.text.b, theme.colors.text.a}; + colors->text_muted = {theme.colors.text_muted.r, theme.colors.text_muted.g, theme.colors.text_muted.b, + theme.colors.text_muted.a}; + colors->accent = {theme.colors.accent.r, theme.colors.accent.g, theme.colors.accent.b, theme.colors.accent.a}; + colors->success = {theme.colors.success.r, theme.colors.success.g, theme.colors.success.b, theme.colors.success.a}; + colors->error = {theme.colors.error.r, theme.colors.error.g, theme.colors.error.b, theme.colors.error.a}; + colors->warning = {theme.colors.warning.r, theme.colors.warning.g, theme.colors.warning.b, theme.colors.warning.a}; +} + +void molten_shaders_init_with_data(const uint32_t *rect_spirv, size_t rect_size, const uint32_t *rounded_rect_spirv, + size_t rounded_rect_size) { + std::vector rectData(rect_spirv, rect_spirv + rect_size / sizeof(uint32_t)); + std::vector roundedData(rounded_rect_spirv, rounded_rect_spirv + rounded_rect_size / sizeof(uint32_t)); + Render::init_shaders_with_data(rectData, roundedData); +} + +} // extern "C"