tirbofish/dropbear · commit
6d99ab132dcaa8b2d89feee0cfcbaeca3b0d2a6b
feature: basic rectangle rendering onto the screen. also start of UI branch. jarvis, run github actions (my build broke, so likely it will break in the action). Unverified
@@ -7,7 +7,7 @@ package.readme = "README.md" resolver = "3" members = [ - "crates/*" + "crates/*", ] [workspace.dependencies] @@ -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" } @@ -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; @@ -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; +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; @@ -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, } @@ -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("("); @@ -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() + } + } +} @@ -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!() + } +} @@ -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}; @@ -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() + } +} @@ -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); @@ -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"); } @@ -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 @@ -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) +data class EntityId(val raw: Long): ID(raw) @@ -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") +} @@ -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) + } + } +} @@ -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, +} @@ -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 @@ -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 +} @@ -0,0 +1,6 @@ +package com.dropbear.ui + +object WidgetType { + const val RECTANGLE = 0 + const val CIRCLE = 1 +} @@ -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) + } +} @@ -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) + } +} @@ -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. */ @@ -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()) +} @@ -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); +} @@ -10,4 +10,5 @@ class DropbearContext( val assetHandle: Long, val sceneLoaderHandle: Long, val physicsEngineHandle: Long, + val uiHandle: Long, ) @@ -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 + } } } @@ -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 + ) +} @@ -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!") + } + } } } @@ -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") +}