diff --git a/Cargo.toml b/Cargo.tomlindex 6652f99..8e2dd93 100644--- a/Cargo.toml+++ b/Cargo.toml@@ -7,7 +7,7 @@ package.readme = "README.md"
resolver = "3"
members = [
- "crates/*"+ "crates/*",
]
[workspace.dependencies]
diff --git a/crates/eucalyptus-core/Cargo.toml b/crates/eucalyptus-core/Cargo.tomlindex 47772d6..b45ebbb 100644--- a/crates/eucalyptus-core/Cargo.toml+++ b/crates/eucalyptus-core/Cargo.toml@@ -7,7 +7,7 @@ repository.workspace = true
readme = "README.md"
[lib]
-crate-type = ["rlib", "cdylib"]+crate-type = ["rlib", "dylib"]
[dependencies]
dropbear-traits = { path = "../dropbear-traits" }
diff --git a/crates/eucalyptus-core/src/lib.rs b/crates/eucalyptus-core/src/lib.rsindex d0cc99e..25f83a6 100644--- a/crates/eucalyptus-core/src/lib.rs+++ b/crates/eucalyptus-core/src/lib.rs@@ -22,6 +22,7 @@ pub mod mesh;
pub mod entity;
pub mod engine;
pub mod transform;
+pub mod ui;
pub use dropbear_macro as macros;
pub use dropbear_traits as traits;
diff --git a/crates/eucalyptus-core/src/ptr.rs b/crates/eucalyptus-core/src/ptr.rsindex 5c3adab..86bae38 100644--- a/crates/eucalyptus-core/src/ptr.rs+++ b/crates/eucalyptus-core/src/ptr.rs@@ -7,6 +7,7 @@ use hecs::World;
use parking_lot::Mutex;
use crate::physics::PhysicsState;
use crate::scene::loading::SceneLoader;
+use crate::ui::{UiContext};
/// A mutable pointer to a [`World`].
///
@@ -41,4 +42,10 @@ pub type SceneLoaderPtr = *const Mutex<SceneLoader>;
/// A mutable pointer to a [`PhysicsState`].
///
/// Defined in `dropbear_common.h` as `PhysicsEngine`
-pub type PhysicsStatePtr = *mut PhysicsState;\ No newline at end of file+pub type PhysicsStatePtr = *mut PhysicsState;++/// A mutable pointer to a [`UiContext`], used for queueing UI components+/// in the scripting module. +/// +/// Defined in `dropbear_common.h` as `UiBufferPtr`. +pub type UiBufferPtr = *const UiContext;\ No newline at end of filediff --git a/crates/eucalyptus-core/src/scripting.rs b/crates/eucalyptus-core/src/scripting.rsindex f2a6965..e8a4d7e 100644--- a/crates/eucalyptus-core/src/scripting.rs+++ b/crates/eucalyptus-core/src/scripting.rs@@ -12,7 +12,7 @@ pub static JVM_ARGS: OnceLock<String> = OnceLock::new();
pub static AWAIT_JDB: OnceLock<bool> = OnceLock::new();
use std::sync::OnceLock;
-use crate::ptr::{AssetRegistryPtr, CommandBufferPtr, InputStatePtr, PhysicsStatePtr, SceneLoaderPtr, WorldPtr};+use crate::ptr::{AssetRegistryPtr, CommandBufferPtr, InputStatePtr, PhysicsStatePtr, SceneLoaderPtr, UiBufferPtr, WorldPtr};
use crate::scripting::jni::JavaContext;
use crate::scripting::native::NativeLibrary;
use crate::states::{Script};
@@ -29,6 +29,7 @@ use dropbear_engine::model::MODEL_CACHE;
use magna_carta::Target;
use crate::scene::loading::SCENE_LOADER;
use crate::types::{CollisionEvent, ContactForceEvent};
+use crate::ui::UI_COMMAND_BUFFER;
/// The target of the script. This can be either a JVM or a native library.
#[derive(Default, Clone, Debug)]
@@ -225,6 +226,8 @@ impl ScriptManager {
let model_cache_ptr = &raw const *MODEL_CACHE;
ASSET_REGISTRY.add_pointer(Const("model_cache"), model_cache_ptr as usize);
+ + let ui_buf = &raw const *UI_COMMAND_BUFFER;
let context = DropbearContext {
world,
@@ -233,6 +236,7 @@ impl ScriptManager {
assets,
scene_loader,
physics_state,
+ ui_buf,
};
if world.is_null() { log::error!("World pointer is null"); }
@@ -966,4 +970,5 @@ pub struct DropbearContext {
pub assets: AssetRegistryPtr,
pub scene_loader: SceneLoaderPtr,
pub physics_state: PhysicsStatePtr,
+ pub ui_buf: UiBufferPtr,
}
\ No newline at end of filediff --git a/crates/eucalyptus-core/src/scripting/jni.rs b/crates/eucalyptus-core/src/scripting/jni.rsindex ca6ee72..5d97c3b 100644--- a/crates/eucalyptus-core/src/scripting/jni.rs+++ b/crates/eucalyptus-core/src/scripting/jni.rs@@ -315,6 +315,7 @@ impl JavaContext {
let asset_handle = context.assets as jlong;
let scene_loader_handle = context.scene_loader as jlong;
let physics_handle = context.physics_state as jlong;
+ let ui_handle = context.ui_buf as jlong;
let args = [
JValue::Long(world_handle),
@@ -322,7 +323,8 @@ impl JavaContext {
JValue::Long(graphics_handle),
JValue::Long(asset_handle),
JValue::Long(scene_loader_handle),
- JValue::Long(physics_handle)+ JValue::Long(physics_handle),+ JValue::Long(ui_handle),
];
let mut sig = String::from("(");
diff --git a/crates/eucalyptus-core/src/ui.rs b/crates/eucalyptus-core/src/ui.rsnew file mode 100644index 0000000..9b01965--- /dev/null+++ b/crates/eucalyptus-core/src/ui.rs@@ -0,0 +1,215 @@+pub mod rect;++use egui::{Rect, Response, Sense};+use once_cell::sync::Lazy;+use parking_lot::Mutex;+use crate::utils::hashmap::StaleTracker;++pub static UI_COMMAND_BUFFER: Lazy<UiContext> = Lazy::new(|| UiContext::new());++pub enum UiCommand {+ Rect {+ id: u64,+ initial: (f32, f32),+ size: (f32, f32),+ corner_radius: f32,+ stroke: egui::Stroke,+ fill: egui::Color32,+ stroke_kind: egui::StrokeKind,+ },+ Circle {+ id: u64,+ center_x: f64,+ center_y: f64,+ radius: f64,+ },+}++pub struct UiContext {+ command_buffer: Mutex<Vec<UiCommand>>,+ currently_rendering: Mutex<StaleTracker<u64, Response>>,+}++impl UiContext {+ pub fn new() -> Self {+ Self {+ command_buffer: Mutex::new(Vec::new()),+ currently_rendering: Mutex::new(StaleTracker::new()),+ }+ }++ pub fn push(&self, command: UiCommand) {+ self.command_buffer.lock().push(command);+ }+}++pub fn poll(ui: &mut egui::Ui) -> anyhow::Result<()> {+ let mut buffer = UI_COMMAND_BUFFER.command_buffer.lock();+ let mut rendering = UI_COMMAND_BUFFER.currently_rendering.lock();+ rendering.tick();+ for cmd in buffer.drain(..) {+ match cmd {+ UiCommand::Rect {+ id,+ initial,+ size,+ corner_radius,+ stroke,+ fill,+ stroke_kind+ } => {+ let (resp, painter) = ui.allocate_painter(size.into(), Sense::hover());++ painter.rect(+ Rect {+ min: initial.into(),+ max: [initial.0 + size.0, initial.1 + size.1].into(),+ },+ corner_radius,+ fill,+ stroke,+ stroke_kind+ );++ rendering.insert(id, resp);+ }+ UiCommand::Circle { .. } => {++ }+ }+ }++ // remove anything past 3 gen+ rendering.remove_stale(3);++ Ok(())+}++pub mod jni {+ #![allow(non_snake_case)]++ use jni::JNIEnv;+ use jni::sys::{jboolean, jlong};+ use jni::objects::JObject;+ use crate::{convert_ptr};+ use crate::scripting::jni::utils::FromJObject;+ use crate::ui::{UiCommand, UiContext};+ use crate::ui::rect::Rect;++ #[unsafe(no_mangle)]+ pub extern "system" fn Java_com_dropbear_ui_UINative_pushRect(+ mut env: JNIEnv,+ _class: jni::objects::JClass,+ ui_buffer_handle: jlong,+ rect: JObject,+ ) {+ let ui = convert_ptr!(ui_buffer_handle => UiContext);++ let rect: Rect = match Rect::from_jobject(&mut env, &rect) {+ Ok(v) => v,+ Err(e) => {+ let _ = env.throw_new("java/lang/RuntimeException", format!("Unable to convert Rectangle->Rect: {:?}", e));+ return;+ }+ };++ ui.push(UiCommand::Rect {+ id: rect.id,+ initial: rect.initial_pos,+ size: rect.size,+ corner_radius: rect.corner_radius,+ stroke: rect.stroke,+ fill: rect.fill,+ stroke_kind: rect.stroke_kind,+ });+ }+ + #[unsafe(no_mangle)]+ pub extern "system" fn Java_com_dropbear_ui_UINative_pushCircle(+ mut env: JNIEnv,+ _class: jni::objects::JClass,+ ui_buffer_handle: jlong,+ circle: JObject,+ ) {+ let ui = convert_ptr!(ui_buffer_handle => UiContext);++ // Extract Circle fields+ let id_obj = match env+ .get_field(&circle, "id", "Lcom/dropbear/utils/ID;")+ .and_then(|v| v.l())+ {+ Ok(obj) => obj,+ Err(e) => {+ let _ = env.throw_new("java/lang/RuntimeException", format!("Failed to get id field: {}", e));+ return;+ }+ };++ let id = match env.call_method(&id_obj, "getId", "()J", &[]).and_then(|v| v.j()) {+ Ok(val) => val as u64,+ Err(e) => {+ let _ = env.throw_new("java/lang/RuntimeException", format!("Failed to get id value: {}", e));+ return;+ }+ };++ let center_obj = match env+ .get_field(&circle, "center", "Lcom/dropbear/math/Vector2d;")+ .and_then(|v| v.l())+ {+ Ok(obj) => obj,+ Err(e) => {+ let _ = env.throw_new("java/lang/RuntimeException", format!("Failed to get center field: {}", e));+ return;+ }+ };++ let center_x = match env.get_field(¢er_obj, "x", "D").and_then(|v| v.d()) {+ Ok(val) => val,+ Err(e) => {+ let _ = env.throw_new("java/lang/RuntimeException", format!("Failed to get center.x: {}", e));+ return;+ }+ };++ let center_y = match env.get_field(¢er_obj, "y", "D").and_then(|v| v.d()) {+ Ok(val) => val,+ Err(e) => {+ let _ = env.throw_new("java/lang/RuntimeException", format!("Failed to get center.y: {}", e));+ return;+ }+ };++ let radius = match env.get_field(&circle, "radius", "D").and_then(|v| v.d()) {+ Ok(val) => val,+ Err(e) => {+ let _ = env.throw_new("java/lang/RuntimeException", format!("Failed to get radius: {}", e));+ return;+ }+ };++ ui.push(UiCommand::Circle {+ id,+ center_x,+ center_y,+ radius,+ });+ }++ #[unsafe(no_mangle)]+ pub extern "system" fn Java_com_dropbear_ui_UINative_wasClicked(+ _env: JNIEnv,+ _class: jni::objects::JClass,+ ui_buffer_handle: jlong,+ id: jlong,+ ) -> jboolean {+ let ui = convert_ptr!(ui_buffer_handle => UiContext);++ let mut rendering = ui.currently_rendering.lock();+ if let Some(response) = rendering.get(&(id as u64)) {+ response.clicked().into()+ } else {+ false.into()+ }+ }+}\ No newline at end of filediff --git a/crates/eucalyptus-core/src/ui/rect.rs b/crates/eucalyptus-core/src/ui/rect.rsnew file mode 100644index 0000000..d31c48c--- /dev/null+++ b/crates/eucalyptus-core/src/ui/rect.rs@@ -0,0 +1,202 @@+use egui::{Stroke, StrokeKind};+use jni::JNIEnv;+use jni::objects::JObject;+use crate::scripting::jni::utils::{FromJObject, ToJObject};+use crate::scripting::result::DropbearNativeResult;+use crate::scripting::native::DropbearNativeError;++/// Maps directly to a `com.dropbear.ui.primitive.Rectangle` Kotlin class+pub struct Rect {+ pub id: u64,+ pub initial_pos: (f32, f32),+ pub size: (f32, f32),+ pub corner_radius: f32,+ pub stroke: Stroke,+ pub fill: egui::Color32,+ pub stroke_kind: StrokeKind,+}++impl FromJObject for Rect {+ fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self>+ where+ Self: Sized+ {+ let class = env+ .find_class("com/dropbear/ui/primitive/Rectangle")+ .map_err(|_| DropbearNativeError::JNIClassNotFound)?;++ if !env+ .is_instance_of(obj, &class)+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?+ {+ return Err(DropbearNativeError::InvalidArgument);+ }++ // Get the ID field+ let id_obj = env+ .get_field(obj, "id", "Lcom/dropbear/utils/ID;")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .l()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;+ + let id = env+ .call_method(&id_obj, "getId", "()J", &[])+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .j()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u64;++ // Get initial (Vector2d)+ let initial_obj = env+ .get_field(obj, "initial", "Lcom/dropbear/math/Vector2d;")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .l()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;++ let initial_x = env+ .get_field(&initial_obj, "x", "D")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .d()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as f32;++ let initial_y = env+ .get_field(&initial_obj, "y", "D")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .d()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as f32;++ // Get width and height+ let width = env+ .get_field(obj, "width", "D")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .d()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as f32;++ let height = env+ .get_field(obj, "height", "D")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .d()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as f32;++ // Get corner radius+ let corner_radius = env+ .get_field(obj, "cornerRadius", "D")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .d()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as f32;++ // Get stroke width+ let stroke_width = env+ .get_field(obj, "stroke", "D")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .d()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as f32;++ // Get fill colour (Colour object)+ let fill_colour_obj = env+ .get_field(obj, "fillColour", "Lcom/dropbear/utils/Colour;")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .l()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;++ let r = env+ .get_field(&fill_colour_obj, "r", "B")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .b()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u8;++ let g = env+ .get_field(&fill_colour_obj, "g", "B")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .b()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u8;++ let b = env+ .get_field(&fill_colour_obj, "b", "B")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .b()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u8;++ let a = env+ .get_field(&fill_colour_obj, "a", "B")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .b()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u8;++ let fill = egui::Color32::from_rgba_unmultiplied(r, g, b, a);++ // Get stroke colour+ let stroke_colour_obj = env+ .get_field(obj, "strokeColour", "Lcom/dropbear/utils/Colour;")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .l()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;++ let stroke_r = env+ .get_field(&stroke_colour_obj, "r", "B")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .b()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u8;++ let stroke_g = env+ .get_field(&stroke_colour_obj, "g", "B")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .b()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u8;++ let stroke_b = env+ .get_field(&stroke_colour_obj, "b", "B")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .b()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u8;++ let stroke_a = env+ .get_field(&stroke_colour_obj, "a", "B")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .b()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u8;++ let stroke_color = egui::Color32::from_rgba_unmultiplied(stroke_r, stroke_g, stroke_b, stroke_a);+ let stroke = Stroke::new(stroke_width, stroke_color);++ // Get stroke kind (enum)+ let stroke_kind_obj = env+ .get_field(obj, "strokeKind", "Lcom/dropbear/ui/StrokeKind;")+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .l()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;++ let stroke_kind_name = env+ .call_method(&stroke_kind_obj, "name", "()Ljava/lang/String;", &[])+ .map_err(|_| DropbearNativeError::JNIFailedToGetField)?+ .l()+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;++ let stroke_kind_str: String = env+ .get_string(&stroke_kind_name.into())+ .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?+ .into();++ let stroke_kind = match stroke_kind_str.as_str() {+ "Inside" => StrokeKind::Inside,+ "Middle" => StrokeKind::Middle,+ "Outside" => StrokeKind::Outside,+ _ => StrokeKind::Middle, // default+ };++ Ok(Rect {+ id,+ initial_pos: (initial_x, initial_y),+ size: (width, height),+ corner_radius,+ stroke,+ fill,+ stroke_kind,+ })+ }+}++impl ToJObject for Rect {+ fn to_jobject<'a>(&self, _env: &mut JNIEnv<'a>) -> DropbearNativeResult<JObject<'a>> {+ todo!()+ }+}\ No newline at end of filediff --git a/crates/eucalyptus-core/src/utils.rs b/crates/eucalyptus-core/src/utils.rsindex 9a8dab0..0ceedb4 100644--- a/crates/eucalyptus-core/src/utils.rs+++ b/crates/eucalyptus-core/src/utils.rs@@ -1,6 +1,7 @@
//! Utility functions and helpers
pub mod option;
+pub mod hashmap;
use crate::states::Node;
use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, relative_path_from_euca};
diff --git a/crates/eucalyptus-core/src/utils/hashmap.rs b/crates/eucalyptus-core/src/utils/hashmap.rsnew file mode 100644index 0000000..cd0c7c4--- /dev/null+++ b/crates/eucalyptus-core/src/utils/hashmap.rs@@ -0,0 +1,247 @@+use std::collections::HashMap;++/// A wrapper for a [HashMap] that iterates its generation for each access. It is able to check+/// for any stale values, and remove them in [StaleTracker::remove_stale].+///+/// # Types+/// * `K` - The key type, must implement `Eq` and `Hash`+/// * `V` - The value type+///+/// # Examples+///+/// ```+/// let mut tracker = eucalyptus_core::utils::hashmap::StaleTracker::new();+///+/// // Insert some values+/// tracker.insert("session_1", "user_data");+/// tracker.insert("session_2", "other_data");+///+/// // Access session_1 to keep it fresh+/// tracker.get(&"session_1");+///+/// // Advance time+/// tracker.tick();+///+/// // session_2 hasn't been accessed, remove entries older than 0 generations+/// let removed = tracker.remove_stale(0);+/// assert_eq!(removed, vec!["session_2"]);+/// ```+pub struct StaleTracker<K, V> {+ map: HashMap<K, (V, usize)>, // (value, last_access_generation)+ current_generation: usize,+}++impl<K: Eq + std::hash::Hash, V> StaleTracker<K, V> {+ /// Creates a new empty `StaleTracker`.+ ///+ /// # Examples+ ///+ /// ```+ /// let tracker: StaleTracker<String, i32> = StaleTracker::new();+ /// ```+ pub fn new() -> Self {+ Self {+ map: HashMap::new(),+ current_generation: 0,+ }+ }++ /// Inserts a key-value pair into the tracker.+ ///+ /// The entry is marked as accessed at the current generation. If the key already+ /// exists, the old value is replaced and its access time is reset to the current generation.+ ///+ /// # Arguments+ ///+ /// * `key` - The key to insert+ /// * `value` - The value to associate with the key+ ///+ /// # Examples+ ///+ /// ```+ /// let mut tracker = StaleTracker::new();+ /// tracker.insert("key", 42);+ /// ```+ pub fn insert(&mut self, key: K, value: V) {+ self.map.insert(key, (value, self.current_generation));+ }++ /// Gets a reference to the value associated with the key.+ ///+ /// This method marks the entry as accessed at the current generation, preventing it+ /// from being considered stale. Returns `None` if the key doesn't exist.+ ///+ /// # Arguments+ ///+ /// * `key` - The key to look up+ ///+ /// # Returns+ ///+ /// * `Some(&V)` if the key exists+ /// * `None` if the key doesn't exist+ ///+ /// # Examples+ ///+ /// ```+ /// let mut tracker = StaleTracker::new();+ /// tracker.insert("key", 42);+ ///+ /// assert_eq!(tracker.get(&"key"), Some(&42));+ /// assert_eq!(tracker.get(&"missing"), None);+ /// ```+ pub fn get(&mut self, key: &K) -> Option<&V> {+ self.map.get_mut(key).map(|(value, generation)| {+ *generation = self.current_generation;+ &*value+ })+ }++ /// Gets a mutable reference to the value associated with the key.+ ///+ /// This method marks the entry as accessed at the current generation. Returns `None`+ /// if the key doesn't exist.+ ///+ /// # Arguments+ ///+ /// * `key` - The key to look up+ ///+ /// # Returns+ ///+ /// * `Some(&mut V)` if the key exists+ /// * `None` if the key doesn't exist+ ///+ /// # Examples+ ///+ /// ```+ /// let mut tracker = StaleTracker::new();+ /// tracker.insert("counter", 0);+ ///+ /// if let Some(value) = tracker.get_mut(&"counter") {+ /// *value += 1;+ /// }+ /// ```+ pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {+ self.map.get_mut(key).map(|(value, generation)| {+ *generation = self.current_generation;+ value+ })+ }++ /// Advances the generation counter by one.+ ///+ /// Call this method periodically (e.g., once per frame, once per request, etc.) to+ /// mark a new time period. Entries that aren't accessed after calling `tick()` will+ /// age and eventually become stale.+ ///+ /// # Examples+ ///+ /// ```+ /// let mut tracker = StaleTracker::new();+ /// tracker.insert("key", 42);+ ///+ /// // Simulate passage of time+ /// tracker.tick();+ /// tracker.tick();+ ///+ /// // "key" is now 2 generations old (hasn't been accessed since insertion)+ /// ```+ pub fn tick(&mut self) {+ self.current_generation += 1;+ }++ /// Removes and returns all entries that haven't been accessed within `max_age` generations.+ ///+ /// An entry is considered stale if `(current_generation - last_access_generation) > max_age`.+ ///+ /// # Arguments+ ///+ /// * `max_age` - Maximum number of generations an entry can go without access before removal+ ///+ /// # Returns+ ///+ /// A vector of keys that were removed+ ///+ /// # Examples+ ///+ /// ```+ /// let mut tracker = StaleTracker::new();+ ///+ /// tracker.insert("fresh", 1);+ /// tracker.insert("stale", 2);+ ///+ /// tracker.get(&"fresh"); // Access this one+ /// tracker.tick(); // Advance generation+ /// tracker.tick(); // Advance again+ ///+ /// // "stale" hasn't been accessed in 2 generations+ /// let removed = tracker.remove_stale(1);+ /// assert!(removed.contains(&"stale"));+ /// assert!(!removed.contains(&"fresh"));+ /// ```+ pub fn remove_stale(&mut self, max_age: usize) -> Vec<K>+ where K: Clone+ {+ let current = self.current_generation;+ let stale_keys: Vec<K> = self.map+ .iter()+ .filter(|(_, (_, generation))| current - generation > max_age)+ .map(|(k, _)| k.clone())+ .collect();++ for key in &stale_keys {+ self.map.remove(key);+ }++ stale_keys+ }++ /// Returns the number of entries currently in the tracker.+ ///+ /// # Examples+ ///+ /// ```+ /// let mut tracker = StaleTracker::new();+ /// assert_eq!(tracker.len(), 0);+ ///+ /// tracker.insert("key", 42);+ /// assert_eq!(tracker.len(), 1);+ /// ```+ pub fn len(&self) -> usize {+ self.map.len()+ }++ /// Returns `true` if the tracker contains no entries.+ ///+ /// # Examples+ ///+ /// ```+ /// let tracker: StaleTracker<String, i32> = StaleTracker::new();+ /// assert!(tracker.is_empty());+ /// ```+ pub fn is_empty(&self) -> bool {+ self.map.is_empty()+ }++ /// Returns the current generation number.+ ///+ /// This can be useful for debugging or understanding how much time has passed.+ ///+ /// # Examples+ ///+ /// ```+ /// let mut tracker = StaleTracker::new();+ /// assert_eq!(tracker.current_generation(), 0);+ ///+ /// tracker.tick();+ /// assert_eq!(tracker.current_generation(), 1);+ /// ```+ pub fn current_generation(&self) -> usize {+ self.current_generation+ }+}++impl<K: Eq + std::hash::Hash, V> Default for StaleTracker<K, V> {+ fn default() -> Self {+ Self::new()+ }+}\ No newline at end of filediff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rsindex 3b1cb2f..5367519 100644--- a/crates/eucalyptus-editor/src/editor/mod.rs+++ b/crates/eucalyptus-editor/src/editor/mod.rs@@ -182,10 +182,26 @@ impl Editor {
eucalyptus_core::utils::start_deadlock_detector();
- let plugin_registry = PluginRegistry::new();+ let mut plugin_registry = PluginRegistry::new();+ if let Err(e) = plugin_registry.load_plugins() {+ warn!("Failed to load plugins: {e}");+ }+
let mut component_registry = ComponentRegistry::new();
+ register_components(&mut component_registry);++ for plugin in plugin_registry.plugins.values_mut() {+ let plugin_id = plugin.id().to_string();+ let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {+ plugin.register_component(&mut component_registry);+ }));- register_components(/*&mut plugin_registry,*/ &mut component_registry);+ if result.is_ok() {+ log::info!("Registered components for plugin '{plugin_id}'");+ } else {+ warn!("Plugin '{plugin_id}' panicked during component registration");+ }+ }
let component_registry = Arc::new(component_registry);
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rsindex cd2e8d8..ad37b95 100644--- a/crates/redback-runtime/src/scene.rs+++ b/crates/redback-runtime/src/scene.rs@@ -446,6 +446,16 @@ impl Scene for PlayMode {
size: egui::vec2(display_width, display_height),
}));
});
++ // render overlay+ egui::Area::new("overlay".into())+ .fixed_pos(egui::pos2(center_x, center_y))+ .show(&graphics.get_egui_context(), |o_ui| {+ // render the scripting overlay+ if let Err(e) = eucalyptus_core::ui::poll(o_ui) {+ log_once::error_once!("Unable to poll the UI: {}", e);+ }+ });
} else {
log::warn!("No such camera exists in the world");
}
diff --git a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.ktindex de0043b..27bed1c 100644--- a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt+++ b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt@@ -4,6 +4,7 @@ import com.dropbear.asset.AssetHandle
import com.dropbear.ffi.NativeEngine
import com.dropbear.input.InputState
import com.dropbear.scene.SceneManager
+import com.dropbear.ui.Ui
internal var exceptionOnError: Boolean = false
var lastErrorMessage: String? = null
@@ -17,6 +18,7 @@ var lastErrorMessage: String? = null
class DropbearEngine(val native: NativeEngine) {
val inputState: InputState = InputState()
val sceneManager: SceneManager = SceneManager()
+ val ui: Ui = Ui()
init {
Companion.native = native
diff --git a/scripting/commonMain/kotlin/com/dropbear/EntityId.kt b/scripting/commonMain/kotlin/com/dropbear/EntityId.ktindex 19dd8f7..6a1aec3 100644--- a/scripting/commonMain/kotlin/com/dropbear/EntityId.kt+++ b/scripting/commonMain/kotlin/com/dropbear/EntityId.kt@@ -1,8 +1,10 @@
package com.dropbear
+import com.dropbear.utils.ID+
/**
* The ID of an entity (represented as a [Long])
*
* @property raw The entity into bits.
*/
-data class EntityId(val raw: Long)\ No newline at end of file+data class EntityId(val raw: Long): ID(raw)\ No newline at end of filediff --git a/scripting/commonMain/kotlin/com/dropbear/ui/Response.kt b/scripting/commonMain/kotlin/com/dropbear/ui/Response.ktnew file mode 100644index 0000000..b4ddac7--- /dev/null+++ b/scripting/commonMain/kotlin/com/dropbear/ui/Response.kt@@ -0,0 +1,62 @@+package com.dropbear.ui++import com.dropbear.input.MouseButton+import com.dropbear.math.Vector2d+import com.dropbear.utils.ID++/**+ * The response given by an element that is drawn.+ */+class Response(+ val elementId: ID,+ private val ui: Ui+) {+ fun clicked(): Boolean = ui.wasClicked(elementId)+ fun clickedBy(button: MouseButton): Boolean = TODO("Not implemented yet")+ fun secondaryClicked(): Boolean = TODO("Not implemented yet")+ fun longTouched(): Boolean = TODO("Not implemented yet")+ fun middleClicked(): Boolean = TODO("Not implemented yet")+ fun doubleClicked(): Boolean = TODO("Not implemented yet")+ fun tripleClicked(): Boolean = TODO("Not implemented yet")+ fun doubleClickedBy(button: MouseButton): Boolean = TODO("Not implemented yet")+ fun tripleClickedBy(button: MouseButton): Boolean = TODO("Not implemented yet")+ fun clickedWithOpenInBackground(): Boolean = TODO("Not implemented yet")+ fun clickedElsewhere(): Boolean = TODO("Not implemented yet")+ fun enabled(): Boolean = TODO("Not implemented yet")+ fun hovered(): Boolean = TODO("Not implemented yet")+ fun hasFocus(): Boolean = TODO("Not implemented yet")+ fun gainedFocus(): Boolean = TODO("Not implemented yet")+ fun lostFocus() {}+ fun requestFocus() {}+ fun surrenderFocus() {}+ fun dragStarted(): Boolean = TODO("Not implemented yet")+ fun dragStartedBy(button: MouseButton): Boolean = TODO("Not implemented yet")+ fun dragged(): Boolean = TODO("Not implemented yet")+ fun draggedBy(button: MouseButton): Boolean = TODO("Not implemented yet")+ fun dragStopped(): Boolean = TODO("Not implemented yet")+ fun dragStoppedBy(button: MouseButton): Boolean = TODO("Not implemented yet")+ fun dragDelta(): Vector2d = TODO("Not implemented yet")+ fun totalDragDelta(): Vector2d = TODO("Not implemented yet")+ fun dragMotion(): Vector2d = TODO("Not implemented yet")++ // todo: payload++ fun interactPointerPos(): Vector2d? = TODO("Not implemented yet")+ fun hoverPos(): Vector2d = TODO("Not implemented yet")+ fun isPointerButtonDownOn(): Boolean = TODO("Not implemented yet")+ fun changed(): Boolean = TODO("Not implemented yet")+ fun markChanged() {}+ fun shouldClose(): Boolean = TODO("Not implemented yet")+ fun setClose() {}+ fun onHoverUi(ui: (Ui) -> Unit): Response = TODO("Not implemented yet")+ fun onDisabledHoverUi(ui: (Ui) -> Unit) : Response = TODO("Not implemented yet")+ fun onHoverUiAtPointer(ui: (Ui) -> Unit) : Response = TODO("Not implemented yet")+ fun showTooltipUi(ui: (Ui) -> Unit) {}+ fun showTooltipText(text: String) {}+ fun isTooltipOpen(): Boolean = TODO("Not implemented yet")+ fun onHoverTextAtPointer(text: String) {}+ fun onHoverText(text: String) {}+ fun onDisabledHoverText(text: String) {}+ fun highlight(): Boolean = TODO("Not implemented yet")+ fun interact(sense: Sense): Response = TODO("Not implemented yet")+}\ No newline at end of filediff --git a/scripting/commonMain/kotlin/com/dropbear/ui/Sense.kt b/scripting/commonMain/kotlin/com/dropbear/ui/Sense.ktnew file mode 100644index 0000000..f247beb--- /dev/null+++ b/scripting/commonMain/kotlin/com/dropbear/ui/Sense.kt@@ -0,0 +1,69 @@+package com.dropbear.ui++/**+ * What sort of interaction is a widget sensitive to?+ */+class Sense(val bit: Int) {+ companion object {+ const val HOVER = 0++ /**+ * Buttons, sliders, windows, …+ */+ const val CLICK = 1 shl 0++ /**+ * Sliders, windows, scroll bars, scroll areas, …+ */+ const val DRAG = 1 shl 1++ /**+ * This widget wants focus.+ *+ * Anything interactive + labels that can be focused for the benefit of screen readers.+ */+ const val FOCUSABLE = 1 shl 2++ /**+ * Senses no clicks or drags. Only senses mouse hover.+ */+ fun hover(): Sense {+ return Sense(HOVER)+ }++ /**+ * Senses no clicks or drags, but can be focused with the keyboard.+ *+ * Used for labels that can be focused for the benefit of screen readers.+ */+ fun focusableNonInteractive(): Sense {+ return Sense(FOCUSABLE)+ }++ /**+ * Sense clicks and hover, but not drags.+ */+ fun click(): Sense {+ return Sense(CLICK or FOCUSABLE)+ }++ /**+ * Sense drags and hover, but not clicks.+ */+ fun drag(): Sense {+ return Sense(DRAG or FOCUSABLE)+ }++ /**+ * Sense both clicks, drags and hover (e.g. a slider or window).+ *+ * Note that this will introduce a latency when dragging,+ * because when the user starts a press egui can't know if this is the start+ * of a click or a drag, and it won't know until the cursor has+ * either moved a certain distance, or the user has released the mouse button.+ */+ fun clickAndDrag(): Sense {+ return Sense(CLICK or FOCUSABLE or DRAG)+ }+ }+}\ No newline at end of filediff --git a/scripting/commonMain/kotlin/com/dropbear/ui/StrokeKind.kt b/scripting/commonMain/kotlin/com/dropbear/ui/StrokeKind.ktnew file mode 100644index 0000000..38663be--- /dev/null+++ b/scripting/commonMain/kotlin/com/dropbear/ui/StrokeKind.kt@@ -0,0 +1,21 @@+package com.dropbear.ui++/**+ * Describes how the stroke of a shape should be painted.+ */+enum class StrokeKind {+ /**+ * The stroke should be painted entirely inside the shape+ */+ Inside,++ /**+ * The stroke should be painted right on the edge of the shape, half inside and half outside.+ */+ Middle,++ /**+ * The stroke should be painted entirely outside the shape+ */+ Outside,+}\ No newline at end of filediff --git a/scripting/commonMain/kotlin/com/dropbear/ui/Ui.kt b/scripting/commonMain/kotlin/com/dropbear/ui/Ui.ktnew file mode 100644index 0000000..b75719a--- /dev/null+++ b/scripting/commonMain/kotlin/com/dropbear/ui/Ui.kt@@ -0,0 +1,28 @@+package com.dropbear.ui++import com.dropbear.math.Vector2d+import com.dropbear.ui.primitive.Circle+import com.dropbear.ui.primitive.Rectangle+import com.dropbear.utils.ID++/**+ * A command buffer to the 2D part of the game. Can be used for HUD and stuff.+ *+ * All content will be rendered the next frame.+ */+class Ui internal constructor() {+ /**+ * Draws/adds a [Widget] to the viewport. Typically used for HUD's and menus.+ *+ * The drawn widget will return a [Response], in which it is alive for 3 frames if+ * not altered or rerendered.+ */+ fun add(widget: Widget): Response {+ return widget.draw(this)+ }+}++internal expect fun Ui.pushRect(rect: Rectangle)+internal expect fun Ui.pushCircle(circle: Circle)++internal expect fun Ui.wasClicked(id: ID): Boolean\ No newline at end of filediff --git a/scripting/commonMain/kotlin/com/dropbear/ui/Widget.kt b/scripting/commonMain/kotlin/com/dropbear/ui/Widget.ktnew file mode 100644index 0000000..c055024--- /dev/null+++ b/scripting/commonMain/kotlin/com/dropbear/ui/Widget.kt@@ -0,0 +1,13 @@+package com.dropbear.ui++import com.dropbear.utils.ID++/**+ * An abstract class all drawable objects must inherit+ */+abstract class Widget(id: ID) {+ /**+ * Draws the object.+ */+ abstract fun draw(ui: Ui) : Response+}\ No newline at end of filediff --git a/scripting/commonMain/kotlin/com/dropbear/ui/WidgetType.kt b/scripting/commonMain/kotlin/com/dropbear/ui/WidgetType.ktnew file mode 100644index 0000000..be1a6d3--- /dev/null+++ b/scripting/commonMain/kotlin/com/dropbear/ui/WidgetType.kt@@ -0,0 +1,6 @@+package com.dropbear.ui++object WidgetType {+ const val RECTANGLE = 0+ const val CIRCLE = 1+}\ No newline at end of filediff --git a/scripting/commonMain/kotlin/com/dropbear/ui/primitive/Circle.kt b/scripting/commonMain/kotlin/com/dropbear/ui/primitive/Circle.ktnew file mode 100644index 0000000..2e71d7e--- /dev/null+++ b/scripting/commonMain/kotlin/com/dropbear/ui/primitive/Circle.kt@@ -0,0 +1,25 @@+package com.dropbear.ui.primitive++import com.dropbear.math.Vector2d+import com.dropbear.ui.Response+import com.dropbear.ui.Ui+import com.dropbear.ui.Widget+import com.dropbear.ui.pushCircle+import com.dropbear.utils.ID++/**+ * Creates a standard circle.+ *+ * @property center The center of the circle.+ * @property radius The distance from the perimeter of the circle to the center/the radius.+ */+class Circle(+ val id: ID,+ var center: Vector2d,+ var radius: Double,+): Widget(id) {+ override fun draw(ui: Ui): Response {+ ui.pushCircle(this)+ return Response(id, ui)+ }+}\ No newline at end of filediff --git a/scripting/commonMain/kotlin/com/dropbear/ui/primitive/Rectangle.kt b/scripting/commonMain/kotlin/com/dropbear/ui/primitive/Rectangle.ktnew file mode 100644index 0000000..146cb8e--- /dev/null+++ b/scripting/commonMain/kotlin/com/dropbear/ui/primitive/Rectangle.kt@@ -0,0 +1,37 @@+package com.dropbear.ui.primitive++import com.dropbear.math.Vector2d+import com.dropbear.ui.Response+import com.dropbear.ui.StrokeKind+import com.dropbear.ui.Ui+import com.dropbear.ui.Widget+import com.dropbear.ui.pushRect+import com.dropbear.utils.Colour+import com.dropbear.utils.ID++/**+ * @property initial The top left position/coordinate of the rectangle.+ * @property width The width of the rectangle+ * @property height The height of the rectangle+ * @property cornerRadius The radius of the corner. The higher the value, the more smoothened out the rect is+ * @property fillColour The colour of inside the rectangle. By default, it is [Colour.WHITE]+ * @property stroke The thickness/width of the stroke.+ * @property strokeColour The colour of the stroke. By default, it is [Colour.BLACK].+ * @property strokeKind Describes the stroke of a shape+ */+class Rectangle(+ val id: ID,+ var initial: Vector2d,+ var width: Double,+ var height: Double,+ var cornerRadius: Double = 0.0,+ var fillColour: Colour = Colour.WHITE,+ var stroke: Double = 0.0,+ var strokeColour: Colour = Colour.BLACK,+ var strokeKind: StrokeKind = StrokeKind.Middle,+): Widget(id) {+ override fun draw(ui: Ui): Response {+ ui.pushRect(this)+ return Response(id, ui)+ }+}\ No newline at end of filediff --git a/scripting/commonMain/kotlin/com/dropbear/utils/Colour.kt b/scripting/commonMain/kotlin/com/dropbear/utils/Colour.ktindex 009014b..b0c6930 100644--- a/scripting/commonMain/kotlin/com/dropbear/utils/Colour.kt+++ b/scripting/commonMain/kotlin/com/dropbear/utils/Colour.kt@@ -17,6 +17,62 @@ class Colour(
var b: UByte,
var a: UByte,
) {
+ companion object {+ val WHITE = Colour(0u, 0u, 0u, 255u)+ val BLACK = Colour(255u, 255u, 255u, 255u)++ /**+ * From a hex value such as `#ABC123`. It sanitises the input (removes the `#`)+ * and converts it to an RGBA colour.+ */+ fun fromHex(hex: String): Colour {+ val cleanHex = hex.removePrefix("#")++ return when (cleanHex.length) {+ 6 -> {+ Colour(+ r = cleanHex.substring(0, 2).toUByte(16),+ g = cleanHex.substring(2, 4).toUByte(16),+ b = cleanHex.substring(4, 6).toUByte(16),+ a = 255u+ )+ }+ 8 -> {+ Colour(+ r = cleanHex.substring(0, 2).toUByte(16),+ g = cleanHex.substring(2, 4).toUByte(16),+ b = cleanHex.substring(4, 6).toUByte(16),+ a = cleanHex.substring(6, 8).toUByte(16)+ )+ }+ 3 -> {+ Colour(+ r = cleanHex.substring(0, 1).repeat(2).toUByte(16),+ g = cleanHex.substring(1, 2).repeat(2).toUByte(16),+ b = cleanHex.substring(2, 3).repeat(2).toUByte(16),+ a = 255u+ )+ }+ 4 -> {+ Colour(+ r = cleanHex.substring(0, 1).repeat(2).toUByte(16),+ g = cleanHex.substring(1, 2).repeat(2).toUByte(16),+ b = cleanHex.substring(2, 3).repeat(2).toUByte(16),+ a = cleanHex.substring(3, 4).repeat(2).toUByte(16)+ )+ }+ else -> throw IllegalArgumentException("Invalid hex color format: $hex")+ }+ }++ /**+ * Converts a Double to a UByte for each colour and creates a new Colour object.+ */+ fun fromDouble(r: Double, g: Double, b: Double, a: Double): Colour {+ return Colour(r.toInt().toUByte(), g.toInt().toUByte(), b.toInt().toUByte(), a.toInt().toUByte())+ }+ }+
/**
* Divides all values by `255` and creates a new [Vector4d] object.
*/
diff --git a/scripting/commonMain/kotlin/com/dropbear/utils/ID.kt b/scripting/commonMain/kotlin/com/dropbear/utils/ID.ktnew file mode 100644index 0000000..6caa08b--- /dev/null+++ b/scripting/commonMain/kotlin/com/dropbear/utils/ID.kt@@ -0,0 +1,34 @@+package com.dropbear.utils++/**+ * Describes all classes that can be supplied as a form of identification, such as that of a hashcode+ * or a number.+ *+ * This is different to [com.dropbear.asset.Handle] as a handle is used for assets, while an [ID] is used+ * for identifying something.+ *+ * @param id The raw id value+ */+open class ID(id: Long) {+ companion object {+ fun fromString(id: String): ID {+ return ID(hashCode().toLong())+ }+ }++ // it has to be like this EntityId already took `raw`.+ private val rawId: Long = id++ fun getId(): Long = rawId++ override fun toString(): String {+ return "ID(raw=${getId()})"+ }+}++/**+ * Converts a [String] into an [ID], allowing to be used for UI.+ */+fun String.asId(): ID {+ return ID(hashCode().toLong())+}\ No newline at end of filediff --git a/scripting/jvmMain/java/com/dropbear/ui/UINative.java b/scripting/jvmMain/java/com/dropbear/ui/UINative.javanew file mode 100644index 0000000..1e920e4--- /dev/null+++ b/scripting/jvmMain/java/com/dropbear/ui/UINative.java@@ -0,0 +1,15 @@+package com.dropbear.ui;++import com.dropbear.EucalyptusCoreLoader;+import com.dropbear.ui.primitive.Circle;+import com.dropbear.ui.primitive.Rectangle;++public class UINative {+ static {+ new EucalyptusCoreLoader().ensureLoaded();+ }++ public static native void pushRect(long uiBufferHandle, Rectangle rect);+ public static native void pushCircle(long uiBufferHandle, Circle circle);+ public static native boolean wasClicked(long uiBufferHandle, long id);+}diff --git a/scripting/jvmMain/kotlin/com/dropbear/ffi/DropbearContext.kt b/scripting/jvmMain/kotlin/com/dropbear/ffi/DropbearContext.ktindex 3804406..4ed40fd 100644--- a/scripting/jvmMain/kotlin/com/dropbear/ffi/DropbearContext.kt+++ b/scripting/jvmMain/kotlin/com/dropbear/ffi/DropbearContext.kt@@ -10,4 +10,5 @@ class DropbearContext(
val assetHandle: Long,
val sceneLoaderHandle: Long,
val physicsEngineHandle: Long,
+ val uiHandle: Long,
)
\ No newline at end of filediff --git a/scripting/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.ktindex 9bcb833..5acd8f6 100644--- a/scripting/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt+++ b/scripting/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt@@ -9,6 +9,7 @@ actual class NativeEngine {
internal var assetHandle: Long = 0L
internal var sceneLoaderHandle: Long = 0L
internal var physicsEngineHandle: Long = 0L
+ internal var uiBufferHandle: Long = 0L
@JvmName("init")
fun init(ctx: DropbearContext) {
@@ -18,6 +19,7 @@ actual class NativeEngine {
this.assetHandle = ctx.assetHandle
this.sceneLoaderHandle = ctx.sceneLoaderHandle
this.physicsEngineHandle = ctx.physicsEngineHandle
+ this.uiBufferHandle = ctx.uiHandle
if (this.worldHandle <= 0L) {
Logger.error("NativeEngine: Error - Invalid world handle received!")
@@ -43,5 +45,9 @@ actual class NativeEngine {
Logger.error("NativeEngine: Error - Invalid physics handle received!")
return
}
+ if (this.uiBufferHandle <= 0L) {+ Logger.error("NativeEngine: Error - Invalid ui command buffer handle received!")+ return+ }
}
}
\ No newline at end of filediff --git a/scripting/jvmMain/kotlin/com/dropbear/ui/Ui.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/ui/Ui.jvm.ktnew file mode 100644index 0000000..fd58d92--- /dev/null+++ b/scripting/jvmMain/kotlin/com/dropbear/ui/Ui.jvm.kt@@ -0,0 +1,24 @@+package com.dropbear.ui++import com.dropbear.DropbearEngine+import com.dropbear.ui.primitive.Circle+import com.dropbear.ui.primitive.Rectangle+import com.dropbear.utils.ID++internal actual fun Ui.wasClicked(id: ID): Boolean {+ return UINative.wasClicked(DropbearEngine.native.uiBufferHandle, id.getId())+}++internal actual fun Ui.pushRect(rect: Rectangle) {+ UINative.pushRect(+ DropbearEngine.native.uiBufferHandle,+ rect+ )+}++internal actual fun Ui.pushCircle(circle: Circle) {+ UINative.pushCircle(+ DropbearEngine.native.uiBufferHandle,+ circle+ )+}\ No newline at end of filediff --git a/scripting/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt b/scripting/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.ktindex 61d52e1..bdc6f7a 100644--- a/scripting/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt+++ b/scripting/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt@@ -19,6 +19,7 @@ actual class NativeEngine {
private var assetHandle: COpaquePointer? = null
private var sceneLoaderHandle: COpaquePointer? = null
private var physicsEngineHandle: COpaquePointer? = null
+ private var uiBufferHandle: COpaquePointer? = null
@Suppress("unused")
fun init(
@@ -30,6 +31,7 @@ actual class NativeEngine {
this.assetHandle = ctx?.assets?.rawValue?.let { interpretCPointer(it) }
this.sceneLoaderHandle = ctx?.scene_loader?.rawValue?.let { interpretCPointer(it) }
this.physicsEngineHandle = ctx?.physics_engine?.rawValue?.let { interpretCPointer(it) }
+ this.uiBufferHandle = ctx?.graphics?.rawValue?.let { interpretCPointer(it) }
// if release, always enable exceptionOnError
if (!Platform.isDebugBinary) {
@@ -66,5 +68,11 @@ actual class NativeEngine {
throw DropbearNativeException("init failed - Invalid physics engine handle received!")
}
}
+ if (this.uiBufferHandle == null) {+ Logger.error("NativeEngine: Error - Invalid ui command buffer engine handle received!")+ if (exceptionOnError) {+ throw DropbearNativeException("init failed - Invalid ui command buffer engine handle received!")+ }+ }
}
}
\ No newline at end of filediff --git a/scripting/nativeMain/kotlin/com/dropbear/ui/Ui.native.kt b/scripting/nativeMain/kotlin/com/dropbear/ui/Ui.native.ktnew file mode 100644index 0000000..e730a47--- /dev/null+++ b/scripting/nativeMain/kotlin/com/dropbear/ui/Ui.native.kt@@ -0,0 +1,18 @@+package com.dropbear.ui++import com.dropbear.DropbearEngine+import com.dropbear.math.Vector2d+import com.dropbear.ui.primitive.Circle+import com.dropbear.ui.primitive.Rectangle+import com.dropbear.utils.ID++internal actual fun Ui.wasClicked(id: ID): Boolean {+ TODO("Not yet implemented")+}+internal actual fun Ui.pushRect(rect: Rectangle) {+ TODO("Not yet implemented")+}++internal actual fun Ui.pushCircle(circle: Circle) {+ TODO("Not yet implemented")+}\ No newline at end of file