tirbofish/dropbear · commit
5eb186d0a77b4391e6bda7c86b23dd4c70f012e5
fix: ensured speed was constant with deltaTime (double FPS meant double speed, not expected). feature: complete the rest of buttons types, more testing needed, still doesnt **really** work Unverified
@@ -77,6 +77,7 @@ yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "ddf7614" } yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "ddf7614" } thiserror = "2.0" tempfile = "3.24" +combine = "4.6" [workspace.dependencies.image] version = "0.25" @@ -240,43 +240,43 @@ impl Camera { self.uniform.view_proj = mvp.as_mat4().to_cols_array_2d(); } - pub fn move_forwards(&mut self) { + pub fn move_forwards(&mut self, dt: f32) { let forward = (self.target - self.eye).normalize(); - self.eye += forward * self.settings.speed; - self.target += forward * self.settings.speed; + self.eye += forward * self.settings.speed * dt as f64; + self.target += forward * self.settings.speed * dt as f64; } - pub fn move_back(&mut self) { + pub fn move_back(&mut self, dt: f32) { let forward = (self.target - self.eye).normalize(); - self.eye -= forward * self.settings.speed; - self.target -= forward * self.settings.speed; + self.eye -= forward * self.settings.speed * dt as f64; + self.target -= forward * self.settings.speed * dt as f64; } - pub fn move_right(&mut self) { + pub fn move_right(&mut self, dt: f32) { let forward = (self.target - self.eye).normalize(); // LH: right = up.cross(forward) let right = self.up.cross(forward).normalize(); - self.eye += right * self.settings.speed; - self.target += right * self.settings.speed; + self.eye += right * self.settings.speed * dt as f64; + self.target += right * self.settings.speed * dt as f64; } - pub fn move_left(&mut self) { + pub fn move_left(&mut self, dt: f32) { let forward = (self.target - self.eye).normalize(); let right = self.up.cross(forward).normalize(); - self.eye -= right * self.settings.speed; - self.target -= right * self.settings.speed; + self.eye -= right * self.settings.speed * dt as f64; + self.target -= right * self.settings.speed * dt as f64; } - pub fn move_up(&mut self) { + pub fn move_up(&mut self, dt: f32) { let up = self.up.normalize(); - self.eye += up * self.settings.speed; - self.target += up * self.settings.speed; + self.eye += up * self.settings.speed * dt as f64; + self.target += up * self.settings.speed * dt as f64; } - pub fn move_down(&mut self) { + pub fn move_down(&mut self, dt: f32) { let up = self.up.normalize(); - self.eye -= up * self.settings.speed; - self.target -= up * self.settings.speed; + self.eye -= up * self.settings.speed * dt as f64; + self.target -= up * self.settings.speed * dt as f64; } pub fn track_mouse_delta(&mut self, dx: f64, dy: f64) { @@ -108,7 +108,7 @@ fn transform_function(mut func: ItemFn) -> ItemFn { let pointer_check = if !is_void { quote! { if out_result.is_null() { - return crate::scripting::native::DropbearNativeError::NullPointer as i32; + return crate::scripting::native::DropbearNativeError::NullPointer.code(); } } } else { @@ -118,11 +118,11 @@ fn transform_function(mut func: ItemFn) -> ItemFn { let success_handling = if !is_void { quote! { unsafe { *out_result = val; } - crate::scripting::native::DropbearNativeError::Success as i32 + crate::scripting::native::DropbearNativeError::Success.code() } } else { quote! { - crate::scripting::native::DropbearNativeError::Success as i32 + crate::scripting::native::DropbearNativeError::Success.code() } }; @@ -139,7 +139,7 @@ fn transform_function(mut func: ItemFn) -> ItemFn { #success_handling } DropbearNativeResult::Err(e) => { - e as i32 + e.code() } } } @@ -44,6 +44,8 @@ rustc_version_runtime.workspace = true rapier3d.workspace = true bytemuck.workspace = true yakui.workspace = true +thiserror.workspace = true +combine.workspace = true [features] default = [] @@ -110,42 +110,4 @@ pub mod jni { ); } } -} - -#[dropbear_macro::impl_c_api] -pub mod native { - use crate::command::CommandBuffer; - use crate::convert_ptr; - use crate::engine::shared::read_str; - use crate::ptr::{AssetRegistryPtr, CommandBufferPtr, WorldPtr}; - use crate::scripting::result::DropbearNativeResult; - use dropbear_engine::asset::AssetRegistry; - use hecs::World; - use std::os::raw::c_char; - - pub fn dropbear_get_entity( - world_ptr: WorldPtr, - label: *const c_char, - ) -> DropbearNativeResult<u64> { - let world = convert_ptr!(world_ptr => World); - let label_str = unsafe { read_str(label)? }; - - super::shared::get_entity(world, &label_str) - } - - pub fn dropbear_get_asset( - asset_ptr: AssetRegistryPtr, - uri: *const c_char, - ) -> DropbearNativeResult<u64> { - let asset_registry = convert_ptr!(asset_ptr => AssetRegistry); - let uri_str = unsafe { read_str(uri)? }; - super::shared::get_asset(&asset_registry, &uri_str) - } - - pub fn dropbear_quit( - command_ptr: CommandBufferPtr, - ) -> DropbearNativeResult<()> { - let sender = convert_ptr!(command_ptr => crossbeam_channel::Sender<CommandBuffer>); - super::shared::quit(sender) - } } @@ -358,193 +358,6 @@ pub mod jni { } } -pub mod native { - use crate::ptr::{CommandBufferPtr, SceneLoaderPtr}; - use crate::scripting::native::DropbearNativeError; - use std::ffi::c_char; - use std::ffi::CStr; - - #[unsafe(no_mangle)] - pub extern "C" fn load_scene_async( - command_buffer_ptr: CommandBufferPtr, - scene_loader_ptr: SceneLoaderPtr, - scene_name: *const c_char, - loading_scene: *const c_char, - out_scene_id: *mut u64, - ) -> DropbearNativeError { - if command_buffer_ptr.is_null() || scene_loader_ptr.is_null() || scene_name.is_null() { - return DropbearNativeError::NullPointer; - } - - let command_buffer = unsafe { &*command_buffer_ptr }; - let scene_loader = unsafe { &*scene_loader_ptr }; - - let scene_name_cstr = unsafe { CStr::from_ptr(scene_name) }; - let scene_name_str = match scene_name_cstr.to_str() { - Ok(s) => s.to_string(), - Err(_) => return DropbearNativeError::InvalidUTF8, - }; - - let loading_scene_option = if !loading_scene.is_null() { - let loading_scene_cstr = unsafe { CStr::from_ptr(loading_scene) }; - match loading_scene_cstr.to_str() { - Ok(s) => Some(s.to_string()), - Err(_) => return DropbearNativeError::InvalidUTF8, - } - } else { - None - }; - - match super::shared::load_scene_async(command_buffer, scene_loader, scene_name_str, loading_scene_option) { - Ok(scene_id) => { - if !out_scene_id.is_null() { - unsafe { *out_scene_id = scene_id }; - } - DropbearNativeError::Success - } - Err(e) => e, - } - } - - #[unsafe(no_mangle)] - pub extern "C" fn switch_to_scene_immediate( - command_buffer_ptr: CommandBufferPtr, - scene_name: *const c_char, - ) -> DropbearNativeError { - if command_buffer_ptr.is_null() || scene_name.is_null() { - return DropbearNativeError::NullPointer; - } - - let command_buffer = unsafe { &*command_buffer_ptr }; - - let scene_name_cstr = unsafe { CStr::from_ptr(scene_name) }; - let scene_name_str = match scene_name_cstr.to_str() { - Ok(s) => s.to_string(), - Err(_) => return DropbearNativeError::InvalidUTF8, - }; - - match super::shared::switch_to_scene_immediate(command_buffer, scene_name_str) { - Ok(_) => DropbearNativeError::Success, - Err(e) => e, - } - } - - #[unsafe(no_mangle)] - pub extern "C" fn get_scene_load_handle_scene_name( - scene_loader_ptr: SceneLoaderPtr, - scene_id: u64, - out_buffer: *mut c_char, - buffer_size: usize, - ) -> DropbearNativeError { - if scene_loader_ptr.is_null() || out_buffer.is_null() { - return DropbearNativeError::NullPointer; - } - - let scene_loader = unsafe { &*scene_loader_ptr }; - - match super::shared::get_scene_load_handle_scene_name(scene_loader, scene_id) { - Ok(scene_name) => { - let c_string = match std::ffi::CString::new(scene_name) { - Ok(cstr) => cstr, - Err(_) => return DropbearNativeError::CStringError, - }; - - let bytes = c_string.as_bytes_with_nul(); - if bytes.len() > buffer_size { - return DropbearNativeError::BufferTooSmall; - } - - unsafe { - std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buffer as *mut u8, bytes.len()); - } - - DropbearNativeError::Success - } - Err(e) => e, - } - } - - #[unsafe(no_mangle)] - pub extern "C" fn switch_to_scene_async( - command_buffer_ptr: CommandBufferPtr, - scene_loader_ptr: SceneLoaderPtr, - scene_id: u64, - ) -> DropbearNativeError { - if command_buffer_ptr.is_null() || scene_loader_ptr.is_null() { - return DropbearNativeError::NullPointer; - } - - let command_buffer = unsafe { &*command_buffer_ptr }; - let scene_loader = unsafe { &*scene_loader_ptr }; - - match super::shared::switch_to_scene_async(command_buffer, scene_loader, scene_id) { - Ok(_) => DropbearNativeError::Success, - Err(e) => e, - } - } - - #[unsafe(no_mangle)] - pub extern "C" fn get_scene_load_progress( - scene_loader_ptr: SceneLoaderPtr, - scene_id: u64, - out_current: *mut f64, - out_total: *mut f64, - out_message: *mut c_char, - message_buffer_size: usize, - ) -> DropbearNativeError { - if scene_loader_ptr.is_null() { - return DropbearNativeError::NullPointer; - } - - let scene_loader = unsafe { &*scene_loader_ptr }; - - match super::shared::get_scene_load_progress(scene_loader, scene_id) { - Ok(progress) => { - if !out_current.is_null() { - unsafe { *out_current = progress.current as f64 }; - } - if !out_total.is_null() { - unsafe { *out_total = progress.total as f64 }; - } - - if !out_message.is_null() && message_buffer_size > 0 { - let message_cstr = match std::ffi::CString::new(progress.message) { - Ok(cstr) => cstr, - Err(_) => return DropbearNativeError::CStringError, - }; - - let bytes = message_cstr.as_bytes_with_nul(); - let copy_len = std::cmp::min(bytes.len(), message_buffer_size); - - unsafe { - std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_message as *mut u8, copy_len); - } - } - - DropbearNativeError::Success - } - Err(e) => e, - } - } - - #[unsafe(no_mangle)] - pub extern "C" fn get_scene_load_status( - scene_loader_ptr: SceneLoaderPtr, - scene_id: u64, - ) -> i32 { - if scene_loader_ptr.is_null() { - return DropbearNativeError::NullPointer as i32; - } - - let scene_loader = unsafe { &*scene_loader_ptr }; - - match super::shared::get_scene_load_status(scene_loader, scene_id) { - Ok(status) => status as i32, - Err(e) => e as i32, - } - } -} - impl crate::scripting::jni::utils::ToJObject for crate::utils::Progress { fn to_jobject<'a>(&self, env: &mut JNIEnv<'a>) -> DropbearNativeResult<JObject<'a>> { let class = env.find_class("com/dropbear/utils/Progress") @@ -10,10 +10,14 @@ use crate::scripting::native::sig::{CollisionEvent, ContactForceEvent, DestroyAl use anyhow::anyhow; use libloading::{Library, Symbol}; use std::ffi::CString; -use std::fmt::{Display, Formatter}; +// use std::fmt::{Display, Formatter}; // Display derived by thiserror use std::path::Path; use crate::scripting::DropbearContext; use crate::types::{CollisionEvent as CollisionEventFFI, ContactForceEvent as ContactForceEventFFI}; +use thiserror::Error; +use jni::signature::TypeSignature; +use jni::errors::JniError; + pub struct NativeLibrary { #[allow(dead_code)] @@ -442,150 +446,199 @@ impl LastErrorMessage for NativeLibrary { } } -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Error)] /// Displays the types of errors that can be returned by the native library. pub enum DropbearNativeError { /// An error in the case the function returns an unsigned value. - /// - /// Subtract [`DropbearNativeError::UnsignedGenericError`] with another value - /// to get the alternative unsigned error. - UnsignedGenericError = 65535, + #[error("Unsigned generic error")] + UnsignedGenericError, /// An error that is thrown, but doesn't have any attached context. - GenericError = 1, + #[error("Generic error")] + GenericError, /// The default return code for a successful FFI operation. - Success = 0, + #[error("Success")] + Success, /// A null pointer was provided into the function, and cannot be read. - NullPointer = -1, + #[error("Null pointer")] + NullPointer, /// Attempting to query the current world failed for some cause. - QueryFailed = -2, + #[error("Query failed")] + QueryFailed, /// The entity does not exist in the world. - EntityNotFound = -3, - - /// No such component exists **or** the component type id is not the same as the one in the - /// [`hecs::World`] database. - /// - /// # Causes - /// There are two potential causes for this error: - /// - If the component that the world is locating is not available within the entity, it will - /// throw this. - /// - Due to Rust's compilation methods and the weird architecture of the dropbear project, - /// if the `eucalyptus_core` library is not compiled with an executable - /// (such as `eucalyptus-editor` or `redback-runtime`), it will throw this error. - /// - /// The querying system of `eucalyptus-core` is done with a [`hecs::World`] (which stored all the - /// entities), the component registry ([`dropbear_traits::registry::ComponentRegistry`]) that - /// stores all the potential component names (including ones from external plugins) and the - /// [`std::any::TypeId`] (which generates a hash of the components/types). - /// - /// If `eucalyptus-core` is externally compiled as its own thing (and not bundled with any executable), - /// a query will lead to a fail due to the hashes being completely different. - /// - /// When originally stumped with the issue of DLL's and EXE constantly throwing this error, a user - /// on the Rust discord provided me with this: - /// - /// ```txt - /// in short, the rules are as follows: - /// 1. If the compilers that produced two Rust binaries are different, or they were produced by - /// compiling for different targets, or if one of them is a compilation root (not of crate - /// type rlib or dylib), the two binaries have different Rust ABI - /// 2. If two binaries have different Rust ABIs, one cannot be used to satisfy a crate dependency - /// of another (and thus, absent extern blocks and the associated unsafe, they cannot call each other's functions) - /// 3. If two binaries have different Rust ABI, their TypeIds will not be consistent - /// 4. If two Rust binaries are built from different source code, one cannot be substituted for - /// another to satisfy the dependency of some other crate - /// ``` - /// - /// Yeah, so likely if this error is thrown at you, either the compilation is wrong or you - /// **genuinely** messed up and didn't include the component. - /// - /// Anyhow, if you are able to confirm it was a compilation error, please open an issue - /// on the dropbear GitHub. - NoSuchComponent = -4, - + #[error("Entity not found")] + EntityNotFound, + /// No such component exists. + #[error("No such component")] + NoSuchComponent, /// No such entity uses the specific component. - NoSuchEntity = -5, + #[error("No such entity")] + NoSuchEntity, /// Inserting something (like a component) into the world failed - WorldInsertError = -6, + #[error("World insert error")] + WorldInsertError, /// When the graphics queue fails to send its message to the receiver - SendError = -7, + #[error("Send error")] + SendError, /// Error while creating a new CString - CStringError = -8, - BufferTooSmall = -9, + #[error("CString error")] + CStringError, + #[error("Buffer too small")] + BufferTooSmall, /// Attempting to switch scenes before the world is loaded will throw this error. - PrematureSceneSwitch = -10, + #[error("Premature scene switch")] + PrematureSceneSwitch, /// When a gamepad is not found while querying the input state for so. - GamepadNotFound = -11, + #[error("Gamepad not found")] + GamepadNotFound, /// When the argument is invalid - InvalidArgument = -12, + #[error("Invalid argument")] + InvalidArgument, /// The handle provided does not exist. Could be for an asset, entity, or other handle type. - NoSuchHandle = -13, + #[error("No such handle")] + NoSuchHandle, /// Failed to create a Java object via JNI. - JNIFailedToCreateObject = -14, + #[error("JNI failed to create object")] + JNIFailedToCreateObject, /// Failed to get a field from a Java object via JNI. - JNIFailedToGetField = -15, + #[error("JNI failed to get field")] + JNIFailedToGetField, /// Failed to find a Java class via JNI. - JNIClassNotFound = -16, + #[error("JNI class not found")] + JNIClassNotFound, /// Failed to find a Java method via JNI. - JNIMethodNotFound = -17, + #[error("JNI method not found")] + JNIMethodNotFound, /// Failed to unwrap a Java object via JNI. - JNIUnwrapFailed = -18, + #[error("JNI unwrap failed")] + JNIUnwrapFailed, /// Generic asset error. There was an error thrown, however there is no context attached. - GenericAssetError = -19, + #[error("Generic asset error")] + GenericAssetError, /// The provided uri (either euca:// or https) was invalid and formatted wrong. - InvalidURI = -20, + #[error("Invalid URI")] + InvalidURI, /// The asset provided by the handle is wrong. - AssetNotFound = -21, + #[error("Asset not found")] + AssetNotFound, /// When a handle has been inputted wrongly. - InvalidHandle = -22, + #[error("Invalid handle")] + InvalidHandle, /// When a physics object is not found - PhysicsObjectNotFound = -23, - - /// The entity provided was invalid, likely not from [hecs::Entity::from_bits]. - InvalidEntity = -100, - - - /// The CString (or `*const c_char`) contained invalid UTF-8 while being decoded. - InvalidUTF8 = -108, - /// A generic error when the library doesn't know what happened or cannot find a - /// suitable error code. - /// - /// The number `1274` comes from the total sum of the word "UnknownError" in decimal - UnknownError = -1274, + #[error("Physics object not found")] + PhysicsObjectNotFound, + /// The entity provided was invalid. + #[error("Invalid entity")] + InvalidEntity, + /// The CString contained invalid UTF-8. + #[error("Invalid UTF-8")] + InvalidUTF8, + /// A generic error when the library doesn't know what happened. + #[error("Unknown error")] + UnknownError, + + // JNI Errors impl + #[error("Invalid JValue type cast: {0}. Actual type: {1}")] + WrongJValueType(&'static str, &'static str), + #[error("Invalid constructor return type (must be void)")] + InvalidCtorReturn, + #[error("Invalid number or type of arguments passed to java method: {0}")] + InvalidArgList(TypeSignature), + #[error("Method not found: {name} {sig}")] + MethodNotFound { name: String, sig: String }, + #[error("Field not found: {name} {sig}")] + FieldNotFound { name: String, sig: String }, + #[error("Java exception was thrown")] + JavaException, + #[error("JNIEnv null method pointer for {0}")] + JNIEnvMethodNotFound(&'static str), + #[error("Null pointer in {0}")] + NullPtr(&'static str), + #[error("Null pointer deref in {0}")] + NullDeref(&'static str), + #[error("Mutex already locked")] + TryLock, + #[error("JavaVM null method pointer for {0}")] + JavaVMMethodNotFound(&'static str), + #[error("Field already set: {0}")] + FieldAlreadySet(String), + #[error("Throw failed with error code {0}")] + ThrowFailed(i32), + #[error("Parse failed for input: {1}")] + ParseFailed(#[source] combine::error::StringStreamError, String), + #[error("JNI call failed")] + JniCall(#[source] JniError), +} + +impl DropbearNativeError { + pub fn code(&self) -> i32 { + match self { + DropbearNativeError::Success => 0, + DropbearNativeError::UnsignedGenericError => 65535, + DropbearNativeError::GenericError => 1, + DropbearNativeError::NullPointer => -1, + DropbearNativeError::QueryFailed => -2, + DropbearNativeError::EntityNotFound => -3, + DropbearNativeError::NoSuchComponent => -4, + DropbearNativeError::NoSuchEntity => -5, + DropbearNativeError::WorldInsertError => -6, + DropbearNativeError::SendError => -7, + DropbearNativeError::CStringError => -8, + DropbearNativeError::BufferTooSmall => -9, + DropbearNativeError::PrematureSceneSwitch => -10, + DropbearNativeError::GamepadNotFound => -11, + DropbearNativeError::InvalidArgument => -12, + DropbearNativeError::NoSuchHandle => -13, + DropbearNativeError::JNIFailedToCreateObject => -14, + DropbearNativeError::JNIFailedToGetField => -15, + DropbearNativeError::JNIClassNotFound => -16, + DropbearNativeError::JNIMethodNotFound => -17, + DropbearNativeError::JNIUnwrapFailed => -18, + DropbearNativeError::GenericAssetError => -19, + DropbearNativeError::InvalidURI => -20, + DropbearNativeError::AssetNotFound => -21, + DropbearNativeError::InvalidHandle => -22, + DropbearNativeError::PhysicsObjectNotFound => -23, + DropbearNativeError::InvalidEntity => -100, + DropbearNativeError::InvalidUTF8 => -108, + DropbearNativeError::UnknownError => -1274, + // New JNI errors start from -200 to separate them + DropbearNativeError::WrongJValueType(_, _) => -200, + DropbearNativeError::InvalidCtorReturn => -201, + DropbearNativeError::InvalidArgList(_) => -202, + DropbearNativeError::MethodNotFound { .. } => -203, + DropbearNativeError::FieldNotFound { .. } => -204, + DropbearNativeError::JavaException => -205, + DropbearNativeError::JNIEnvMethodNotFound(_) => -206, + DropbearNativeError::NullPtr(_) => -207, + DropbearNativeError::NullDeref(_) => -208, + DropbearNativeError::TryLock => -209, + DropbearNativeError::JavaVMMethodNotFound(_) => -210, + DropbearNativeError::FieldAlreadySet(_) => -211, + DropbearNativeError::ThrowFailed(_) => -212, + DropbearNativeError::ParseFailed(_, _) => -213, + DropbearNativeError::JniCall(_) => -214, + } + } } -impl Display for DropbearNativeError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - DropbearNativeError::NullPointer => "NullPointer (-1)", - DropbearNativeError::QueryFailed => "QueryFailed (-2)", - DropbearNativeError::EntityNotFound => "EntityNotFound (-3)", - DropbearNativeError::NoSuchComponent => "NoSuchComponent (-4)", - DropbearNativeError::NoSuchEntity => "NoSuchEntity (-5)", - DropbearNativeError::WorldInsertError => "WorldInsertError (-6)", - DropbearNativeError::SendError => "SendError (-7)", - DropbearNativeError::InvalidUTF8 => "InvalidUTF8 (-108)", - DropbearNativeError::UnknownError => "UnknownError (-1274)", - DropbearNativeError::UnsignedGenericError => "UnsignedGenericError (65535)", - DropbearNativeError::Success => "Success (0) [should not be displayed]", - DropbearNativeError::CStringError => "CStringError (-8)", - DropbearNativeError::BufferTooSmall => "BufferTooSmall (-9)", - DropbearNativeError::PrematureSceneSwitch => "PrematureSceneSwitch (-10)", - DropbearNativeError::GamepadNotFound => "GamepadNotFound (-11)", - DropbearNativeError::InvalidArgument => "InvalidArgument (-12)", - DropbearNativeError::NoSuchHandle => "NoSuchHandle (-13)", - DropbearNativeError::JNIFailedToCreateObject => "JNIFailedToCreateObject (-14)", - DropbearNativeError::JNIFailedToGetField => "JNIFailedToGetField (-15)", - DropbearNativeError::JNIClassNotFound => "JNIClassNotFound (-16)", - DropbearNativeError::JNIMethodNotFound => "JNIMethodNotFound (-17)", - DropbearNativeError::JNIUnwrapFailed => "JNIUnwrapFailed (-18)", - DropbearNativeError::InvalidEntity => "InvalidEntity (-100)", - DropbearNativeError::GenericAssetError => "GenericAssetError (-19)", - DropbearNativeError::InvalidURI => "InvalidURI (-20)", - DropbearNativeError::AssetNotFound => "AssetNotFound (-21)", - DropbearNativeError::InvalidHandle => "InvalidHandle (-22)", - DropbearNativeError::GenericError => "GenericError (1)", - DropbearNativeError::PhysicsObjectNotFound => "PhysicsObjectNotFound (-23)", - }) +impl From<jni::errors::Error> for DropbearNativeError { + fn from(err: jni::errors::Error) -> Self { + match err { + jni::errors::Error::WrongJValueType(a, b) => DropbearNativeError::WrongJValueType(a, b), + jni::errors::Error::InvalidCtorReturn => DropbearNativeError::InvalidCtorReturn, + jni::errors::Error::InvalidArgList(s) => DropbearNativeError::InvalidArgList(s), + jni::errors::Error::MethodNotFound { name, sig } => DropbearNativeError::MethodNotFound { name, sig }, + jni::errors::Error::FieldNotFound { name, sig } => DropbearNativeError::FieldNotFound { name, sig }, + jni::errors::Error::JavaException => DropbearNativeError::JavaException, + jni::errors::Error::JNIEnvMethodNotFound(s) => DropbearNativeError::JNIEnvMethodNotFound(s), + jni::errors::Error::NullPtr(s) => DropbearNativeError::NullPtr(s), + jni::errors::Error::NullDeref(s) => DropbearNativeError::NullDeref(s), + jni::errors::Error::TryLock => DropbearNativeError::TryLock, + jni::errors::Error::JavaVMMethodNotFound(s) => DropbearNativeError::JavaVMMethodNotFound(s), + jni::errors::Error::FieldAlreadySet(s) => DropbearNativeError::FieldAlreadySet(s), + jni::errors::Error::ThrowFailed(i) => DropbearNativeError::ThrowFailed(i), + jni::errors::Error::ParseFailed(e, s) => DropbearNativeError::ParseFailed(e, s), + jni::errors::Error::JniCall(e) => DropbearNativeError::JniCall(e), + } } -} +} @@ -1,10 +1,17 @@ +mod button; +mod utils; + use std::cell::RefCell; +use ::jni::JNIEnv; +use ::jni::objects::JObject; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; -use yakui::Yakui; +use yakui::{Yakui}; use dropbear_engine::utils::ResourceReference; use dropbear_macro::SerializableComponent; use dropbear_traits::SerializableComponent; +use crate::scripting::jni::utils::{FromJObject}; +use crate::scripting::result::DropbearNativeResult; thread_local! { pub static UI_CONTEXT: RefCell<UiContext> = RefCell::new(UiContext::new()); @@ -24,34 +31,66 @@ pub struct UIComponent { // note for tomorrow: use UIInstruction like that of asm -#[derive(Default, Debug)] -pub enum UIInstruction { - #[default] - Nothing, +pub trait NativeWidget: Send + std::fmt::Debug { + fn build(self: Box<Self>); +} + +impl NativeWidget for yakui::widgets::Button { + fn build(mut self: Box<Self>) { + let _ = self.show(); + } +} + +pub trait WidgetParser: Send + Sync { + fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<Box<dyn NativeWidget>>>; +} + +static PARSERS: std::sync::OnceLock<Mutex<Vec<Box<dyn WidgetParser>>>> = std::sync::OnceLock::new(); - StartColumn, - EndColumn, +pub fn register_widget_parser<P: WidgetParser + 'static>(parser: P) { + let parsers = PARSERS.get_or_init(|| Mutex::new(Vec::new())); + parsers.lock().push(Box::new(parser)); } +fn get_parsers() -> &'static Mutex<Vec<Box<dyn WidgetParser>>> { + PARSERS.get_or_init(|| { + let mut vec: Vec<Box<dyn WidgetParser>> = Vec::new(); + vec.push(Box::new(ButtonParser)); + Mutex::new(vec) + }) +} + +struct ButtonParser; + +impl WidgetParser for ButtonParser { + fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<Box<dyn NativeWidget>>> { + let class = env.get_object_class(obj)?; + let name_str_obj = env.call_method(class, "getName", "()Ljava/lang/String;", &[])?.l()?; + let name_string: String = env.get_string(&name_str_obj.into())?.into(); + + if name_string.contains("ButtonInstruction$Button") { + let button_obj = env.get_field(obj, "button", "Lcom/dropbear/ui/widgets/Button;")?.l()?; + let btn = yakui::widgets::Button::from_jobject(env, &button_obj)?; + return Ok(Some(Box::new(btn))); + } + + Ok(None) + } +} + + pub struct UiContext { pub yakui_state: Mutex<Yakui>, - pub instruction_set: Mutex<Vec<UIInstruction>>, + pub instruction_set: Mutex<Vec<Box<dyn NativeWidget>>>, } pub fn poll() { UI_CONTEXT.with(|v| { let ctx = v.borrow(); - let instructions = ctx.instruction_set.lock().drain(..).collect::<Vec<UIInstruction>>(); - for i in instructions { - match i { - UIInstruction::StartColumn => { - - }, - UIInstruction::EndColumn => { - - }, - UIInstruction::Nothing => {} - } + let mut instructions = ctx.instruction_set.lock(); + let current_instructions = instructions.drain(..).collect::<Vec<Box<dyn NativeWidget>>>(); + for i in current_instructions { + i.build(); } }); } @@ -65,30 +104,55 @@ impl UiContext { } } +pub trait UiWidgetType: FromJObject { + type UIWidgetType; + + fn as_id(&self) -> u32; + fn from_id(id: u32) -> Self::UIWidgetType; +} + pub mod jni { use jni::sys::jlong; + use jni::objects::{JClass, JObjectArray}; + use jni::JNIEnv; use crate::convert_ptr; - use crate::ui::{UiContext}; + use crate::ui::{UiContext, get_parsers}; + use crate::scripting::jni::utils::FromJObject; #[unsafe(no_mangle)] - pub extern "C" fn Java_foobar_addOverlay( - _env: jni::JNIEnv, - _class: jni::objects::JClass, + pub extern "system" fn Java_com_dropbear_ui_UINative_renderUI( + mut env: JNIEnv, + _class: JClass, ui_buf_ptr: jlong, + instructions: JObjectArray, ) { let ui = convert_ptr!(ui_buf_ptr => UiContext); + let mut rust_instructions = Vec::new(); - ui.instruction_set.lock().push(crate::ui::UIInstruction::Nothing); - } + let count = env.get_array_length(&instructions).unwrap_or(0); + let parsers_guard = get_parsers().lock(); - #[unsafe(no_mangle)] - pub extern "C" fn Java_foobar_aisClicked( - _env: jni::JNIEnv, - _class: jni::objects::JClass, - ui_buf_ptr: jlong, - ) { - let _ui = convert_ptr!(ui_buf_ptr => UiContext); + for i in 0..count { + let obj = match env.get_object_array_element(&instructions, i) { + Ok(o) => o, + Err(_) => continue, + }; + if obj.is_null() { continue; } + + for parser in parsers_guard.iter() { + match parser.parse(&mut env, &obj) { + Ok(Some(widget)) => { + rust_instructions.push(widget); + break; + }, + Ok(None) => continue, + Err(e) => { + eprintln!("Error converting UI instruction: {:?}", e); + } + } + } + } - // ui.yakui_state.lock() + ui.instruction_set.lock().extend(rust_instructions); } } @@ -0,0 +1,61 @@ +use jni::JNIEnv; +use jni::objects::JObject; +use yakui::{Alignment, BorderRadius}; +use yakui::widgets::{Button, Pad, DynamicButtonStyle}; +use crate::scripting::jni::utils::FromJObject; +use crate::scripting::result::DropbearNativeResult; +use std::borrow::Cow; + +impl FromJObject for Button { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let text_field = env.get_field(obj, "text", "Ljava/lang/String;")?; + let text_jstring = text_field.l()?.into(); + let text: String = env.get_string(&text_jstring)?.into(); + + let f = env.get_field(obj, "padding", "Lcom/dropbear/ui/styling/Padding;")?.l()?; + let padding = Pad::from_jobject( + env, + &f + )?; + + let f = env.get_field(obj, "alignment", "Lcom/dropbear/ui/styling/Alignment;")?.l()?; + let alignment = Alignment::from_jobject( + env, + &f + )?; + + let f = env.get_field(obj, "borderRadius", "Lcom/dropbear/ui/styling/BorderRadius;")?.l()?; + let border_radius = BorderRadius::from_jobject( + env, + &f + )?; + + let f = env.get_field(obj, "style", "Lcom/dropbear/ui/styling/DynamicButtonStyle;")?.l()?; + let style = DynamicButtonStyle::from_jobject( + env, + &f + )?; + + let f = env.get_field(obj, "hoverStyle", "Lcom/dropbear/ui/styling/DynamicButtonStyle;")?.l()?; + let hover_style = DynamicButtonStyle::from_jobject( + env, + &f + )?; + + let f = env.get_field(obj, "downStyle", "Lcom/dropbear/ui/styling/DynamicButtonStyle;")?.l()?; + let down_style = DynamicButtonStyle::from_jobject( + env, + &f + )?; + + Ok(Self { + text: Cow::Owned(text), + alignment, + padding, + border_radius, + style, + hover_style, + down_style, + }) + } +} @@ -0,0 +1,277 @@ +use jni::JNIEnv; +use jni::objects::{JObject, JByteArray}; +use crate::scripting::jni::utils::FromJObject; +use crate::scripting::result::DropbearNativeResult; +use yakui::Color; +use yakui::widgets::{Pad, DynamicButtonStyle}; +use yakui::{Alignment, BorderRadius}; +use yakui::style::{TextStyle, TextAlignment}; +use yakui::cosmic_text::{Attrs, AttrsOwned, FamilyOwned, Weight, Style, Stretch, CacheKeyFlags, FontFeatures, Feature, FeatureTag}; + +impl FromJObject for FeatureTag { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let tag_obj = env.call_method(obj, "asBytes", "()[B", &[])?.l()?; + let tag_bytes = env.convert_byte_array(JByteArray::from(tag_obj))?; + + let mut fixed_tag = [0u8; 4]; + let len = tag_bytes.len().min(4); + fixed_tag[..len].copy_from_slice(&tag_bytes[..len]); + + Ok(FeatureTag::new(&fixed_tag)) + } +} + +impl FromJObject for Feature { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let tag_obj = env.get_field(obj, "tag", "Lcom/dropbear/ui/styling/fonts/FeatureTag;")?.l()?; + let tag = FeatureTag::from_jobject(env, &tag_obj)?; + + let value_obj = env.get_field(obj, "value", "Lcom/dropbear/ui/styling/fonts/UInt;")?.l()?; + let value = env.get_field(&value_obj, "value", "I")?.i()? as u32; + + Ok(Feature { + tag, + value, + }) + } +} + +impl FromJObject for FontFeatures { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let features_list_obj = env.get_field(obj, "features", "Ljava/util/List;")?.l()?; + + let size = env.call_method(&features_list_obj, "size", "()I", &[])?.i()?; + let mut features = Vec::with_capacity(size as usize); + + for i in 0..size { + let item = env.call_method(&features_list_obj, "get", "(I)Ljava/lang/Object;", &[i.into()])?.l()?; + if !item.is_null() { + features.push(Feature::from_jobject(env, &item)?); + } + } + + Ok(FontFeatures { features }) + } +} + +impl FromJObject for Color { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let r = env.get_field(obj, "r", "B")?.b()? as u8; + let g = env.get_field(obj, "g", "B")?.b()? as u8; + let b = env.get_field(obj, "b", "B")?.b()? as u8; + let a = env.get_field(obj, "a", "B")?.b()? as u8; + Ok(Color::rgba(r, g, b, a)) + } +} + +impl FromJObject for Pad { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let left = env.get_field(obj, "left", "D")?.d()? as f32; + let right = env.get_field(obj, "right", "D")?.d()? as f32; + let top = env.get_field(obj, "top", "D")?.d()? as f32; + let bottom = env.get_field(obj, "bottom", "D")?.d()? as f32; + Ok(Pad { + left, + right, + top, + bottom, + }) + } +} + +impl FromJObject for BorderRadius { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let top_left = env.get_field(obj, "topLeft", "D")?.d()? as f32; + let top_right = env.get_field(obj, "topRight", "D")?.d()? as f32; + let bottom_left = env.get_field(obj, "bottomLeft", "D")?.d()? as f32; + let bottom_right = env.get_field(obj, "bottomRight", "D")?.d()? as f32; + Ok(BorderRadius { + top_left, + top_right, + bottom_left, + bottom_right, + }) + } +} + +impl FromJObject for Alignment { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let x = env.get_field(obj, "x", "D")?.d()? as f32; + let y = env.get_field(obj, "y", "D")?.d()? as f32; + Ok(Alignment::new(x, y)) + } +} + +impl FromJObject for TextAlignment { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let name_obj = env.call_method(obj, "name", "()Ljava/lang/String;", &[])?.l()?; + let name: String = env.get_string(&name_obj.into())?.into(); + match name.as_str() { + "Start" => Ok(TextAlignment::Start), + "Center" => Ok(TextAlignment::Center), + "End" => Ok(TextAlignment::End), + _ => Ok(TextAlignment::Start), + } + } +} + +impl FromJObject for Weight { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let val = env.get_field(obj, "value", "I")?.i()? as u16; + Ok(Weight(val)) + } +} + +impl FromJObject for Style { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let name_obj = env.call_method(obj, "name", "()Ljava/lang/String;", &[])?.l()?; + let name: String = env.get_string(&name_obj.into())?.into(); + match name.as_str() { + "Normal" => Ok(Style::Normal), + "Italic" => Ok(Style::Italic), + "Oblique" => Ok(Style::Oblique), + _ => Ok(Style::Normal), + } + } +} + +impl FromJObject for Stretch { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let name_obj = env.call_method(obj, "name", "()Ljava/lang/String;", &[])?.l()?; + let name: String = env.get_string(&name_obj.into())?.into(); + match name.as_str() { + "UltraCondensed" => Ok(Stretch::UltraCondensed), + "ExtraCondensed" => Ok(Stretch::ExtraCondensed), + "Condensed" => Ok(Stretch::Condensed), + "SemiCondensed" => Ok(Stretch::SemiCondensed), + "Normal" => Ok(Stretch::Normal), + "SemiExpanded" => Ok(Stretch::SemiExpanded), + "Expanded" => Ok(Stretch::Expanded), + "ExtraExpanded" => Ok(Stretch::ExtraExpanded), + "UltraExpanded" => Ok(Stretch::UltraExpanded), + _ => Ok(Stretch::Normal), + } + } +} + +impl FromJObject for FamilyOwned { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$Name")? { + let value_obj = env.call_method(obj, "getValue", "()Ljava/lang/String;", &[])?.l()?; + let val: String = env.get_string(&value_obj.into())?.into(); + return Ok(FamilyOwned::Name(val.parse().unwrap())); + } + if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$Serif")? { return Ok(FamilyOwned::Serif); } + if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$SansSerif")? { return Ok(FamilyOwned::SansSerif); } + if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$Cursive")? { return Ok(FamilyOwned::Cursive); } + if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$Fantasy")? { return Ok(FamilyOwned::Fantasy); } + if env.is_instance_of(obj, "com/dropbear/ui/styling/fonts/Family$Monospace")? { return Ok(FamilyOwned::Monospace); } + + Ok(FamilyOwned::SansSerif) + } +} + +impl FromJObject for AttrsOwned { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let family_obj = env.get_field(obj, "family", "Lcom/dropbear/ui/styling/fonts/Family;")?.l()?; + let family_owned = FamilyOwned::from_jobject(env, &family_obj)?; + let family = family_owned.as_family(); + + let color_obj = env.get_field(obj, "colourOptions", "Lcom/dropbear/utils/Colour;")?.l()?; + let color_opt = if !color_obj.is_null() { + let colour = Color::from_jobject(env, &color_obj)?; + Some(yakui::cosmic_text::Color::rgba(colour.r, colour.g, colour.b, colour.a)) + } else { + None + }; + + let stretch_obj = env.get_field(obj, "stretch", "Lcom/dropbear/ui/styling/fonts/Stretch;")?.l()?; + let stretch = Stretch::from_jobject(env, &stretch_obj)?; + + let style_obj = env.get_field(obj, "style", "Lcom/dropbear/ui/styling/fonts/FontStyle;")?.l()?; + let style = Style::from_jobject(env, &style_obj)?; + + let weight_obj = env.get_field(obj, "weight", "Lcom/dropbear/ui/styling/fonts/FontWeight;")?.l()?; + let weight = Weight::from_jobject(env, &weight_obj)?; + + let metadata = match env.get_field(obj, "metadata", "I") { + Ok(val) => val.i()? as usize, + Err(_) => 0, + }; + + let letter_spacing_obj = env.get_field(obj, "letterSpacingOptions", "Ljava/lang/Double;")?.l()?; + let letter_spacing_opt = if !letter_spacing_obj.is_null() { + let val = env.call_method(&letter_spacing_obj, "doubleValue", "()D", &[])?.d()?; + Some(yakui::cosmic_text::LetterSpacing(val as f32)) + } else { + None + }; + + let font_features_obj = env.get_field(obj, "fontFeatures", "Lcom/dropbear/ui/styling/fonts/FontFeatures;")?.l()?; + let font_features = if !font_features_obj.is_null() { + FontFeatures::from_jobject(env, &font_features_obj)? + } else { + FontFeatures::default() + }; + + Ok(AttrsOwned::new(&Attrs { + family, + stretch, + style, + weight, + metadata, + color_opt, + cache_key_flags: CacheKeyFlags::empty(), + metrics_opt: None, + letter_spacing_opt, + font_features, + })) + } +} + +impl FromJObject for TextStyle { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let align_obj = env.get_field(obj, "align", "Lcom/dropbear/ui/styling/fonts/TextAlignment;")?.l()?; + let align = TextAlignment::from_jobject(env, &align_obj)?; + + let font_size = env.get_field(obj, "fontSize", "D")?.d()? as f32; + + let color_obj = env.get_field(obj, "colour", "Lcom/dropbear/utils/Colour;")?.l()?; + let color = if !color_obj.is_null() { + Color::from_jobject(env, &color_obj)? + } else { + Color::WHITE + }; + + Ok(TextStyle { + align, + font_size, + color, + ..TextStyle::default() + }) + } +} + +impl FromJObject for DynamicButtonStyle { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let text_obj = env.get_field(obj, "text", "Lcom/dropbear/ui/styling/TextStyle;")?.l()?; + let text = if !text_obj.is_null() { + TextStyle::from_jobject(env, &text_obj)? + } else { + TextStyle::default() + }; + + let fill_obj = env.get_field(obj, "fill", "Lcom/dropbear/utils/Colour;")?.l()?; + let fill = if !fill_obj.is_null() { + Color::from_jobject(env, &fill_obj)? + } else { + Color::GRAY + }; + + Ok(DynamicButtonStyle { + text, + fill, + border: None, + }) + } +} @@ -56,8 +56,7 @@ impl InspectableComponent for CameraComponent { ui.label("Speed:"); ui.add( egui::DragValue::new(&mut self.settings.speed) - .speed(0.1) - .range(0.1..=20.0), + .speed(0.1), ); }); @@ -66,7 +65,7 @@ impl InspectableComponent for CameraComponent { ui.add( egui::DragValue::new(&mut self.settings.sensitivity) .speed(0.0001) - .range(0.0001..=1.0), + .range(0.0001..=1.5), ); }); @@ -232,12 +232,12 @@ impl Scene for Editor { { for key in &self.input_state.pressed_keys { match key { - KeyCode::KeyW => camera.move_forwards(), - KeyCode::KeyA => camera.move_left(), - KeyCode::KeyD => camera.move_right(), - KeyCode::KeyS => camera.move_back(), - KeyCode::ShiftLeft => camera.move_down(), - KeyCode::Space => camera.move_up(), + KeyCode::KeyW => camera.move_forwards(dt), + KeyCode::KeyA => camera.move_left(dt), + KeyCode::KeyD => camera.move_right(dt), + KeyCode::KeyS => camera.move_back(dt), + KeyCode::ShiftLeft => camera.move_down(dt), + KeyCode::Space => camera.move_up(dt), _ => {} } } @@ -0,0 +1,9 @@ +package com.dropbear.ui.styling + +import com.dropbear.utils.Colour + +data class Border( + val colour: Colour, + val width: Double, +) { +} @@ -0,0 +1,56 @@ +package com.dropbear.ui.styling + +class BorderRadius( + topLeft: Double = 0.0, + topRight: Double = 0.0, + bottomLeft: Double = 0.0, + bottomRight: Double = 0.0 +) { + companion object { + fun uniform(value: Double): BorderRadius { + UInt + return BorderRadius( + value, + value, + value, + value, + ) + } + + fun top(value: Double): BorderRadius { + return BorderRadius( + value, + value, + 0.0, + 0.0, + ) + } + + fun bottom(value: Double): BorderRadius { + return BorderRadius( + 0.0, + 0.0, + value, + value, + ) + } + + fun left(value: Double): BorderRadius { + return BorderRadius( + value, + 0.0, + value, + 0.0, + ) + } + + fun right(value: Double): BorderRadius { + return BorderRadius( + 0.0, + value, + 0.0, + value, + ) + } + } +} @@ -5,4 +5,5 @@ import com.dropbear.utils.Colour data class DynamicButtonStyle( var text: TextStyle = TextStyle.label(), var fill: Colour = Colour.GRAY, + var border: Border? = null, ) @@ -1,5 +1,6 @@ package com.dropbear.ui.styling +import com.dropbear.ui.styling.fonts.FontAttributes import com.dropbear.ui.styling.fonts.FontName import com.dropbear.ui.styling.fonts.TextAlignment import com.dropbear.utils.Colour @@ -9,6 +10,7 @@ data class TextStyle( var fontSize: Double, var colour: Colour, var align: TextAlignment, + var attrs: FontAttributes, ) { companion object { fun label(): TextStyle { @@ -16,7 +18,8 @@ data class TextStyle( font = FontName("default"), fontSize = 14.0, colour = Colour.WHITE, - align = TextAlignment.Start + align = TextAlignment.Start, + attrs = FontAttributes() ) } } @@ -0,0 +1,16 @@ +package com.dropbear.ui.styling.fonts + +import kotlin.jvm.JvmInline + +@JvmInline +value class CacheKeyFlags(val bits: Int = 0) { + operator fun plus(flag: CacheKeyFlags): CacheKeyFlags = + CacheKeyFlags(bits or flag.bits) + + operator fun contains(flag: CacheKeyFlags): Boolean = + (bits and flag.bits) != 0 + + companion object { + val NONE = CacheKeyFlags(0) + } +} @@ -0,0 +1,6 @@ +package com.dropbear.ui.styling.fonts + +class CacheMetrics( + val fontSizeBits: UInt, + val lineHeightBits: UInt, +) @@ -0,0 +1,10 @@ +package com.dropbear.ui.styling.fonts + +sealed interface Family { + data class Name(val value: String) : Family + data object Serif : Family + data object SansSerif : Family + data object Cursive : Family + data object Fantasy : Family + data object Monospace : Family +} @@ -0,0 +1,33 @@ +package com.dropbear.ui.styling.fonts + +import kotlin.jvm.JvmInline + +@JvmInline +value class FeatureTag(val tag: ByteArray) { + init { + require(tag.size == 4) { "Tag must be exactly 4 bytes" } + } + + companion object { + /** Kerning adjusts spacing between specific character pairs */ + val KERNING = FeatureTag("kern".encodeToByteArray()) + /** Standard ligatures (fi, fl, etc.) */ + val STANDARD_LIGATURES = FeatureTag("liga".encodeToByteArray()) + /** Contextual ligatures (context-dependent ligatures) */ + val CONTEXTUAL_LIGATURES = FeatureTag("clig".encodeToByteArray()) + /** Contextual alternates (glyph substitutions based on context) */ + val CONTEXTUAL_ALTERNATES = FeatureTag("calt".encodeToByteArray()) + /** Discretionary ligatures (optional stylistic ligatures) */ + val DISCRETIONARY_LIGATURES = FeatureTag("dlig".encodeToByteArray()) + /** Small caps (lowercase to small capitals) */ + val SMALL_CAPS = FeatureTag("smcp".encodeToByteArray()) + /** All small caps (uppercase and lowercase to small capitals) */ + val ALL_SMALL_CAPS = FeatureTag("c2sc".encodeToByteArray()) + /** Stylistic Set 1 (font-specific alternate glyphs) */ + val STYLISTIC_SET_1 = FeatureTag("ss01".encodeToByteArray()) + /** Stylistic Set 2 (font-specific alternate glyphs) */ + val STYLISTIC_SET_2 = FeatureTag("ss02".encodeToByteArray()) + } + + fun asBytes(): ByteArray = tag +} @@ -0,0 +1,16 @@ +package com.dropbear.ui.styling.fonts + +import com.dropbear.utils.Colour + +class FontAttributes( + var colourOptions: Colour? = null, + var family: Family = Family.SansSerif, + var stretch: Stretch = Stretch.Normal, + var style: FontStyle = FontStyle.Normal, + var weight: FontWeight = FontWeight.NORMAL, + var metadata: UInt = 0u, + var cacheKeyFlags: CacheKeyFlags = CacheKeyFlags.NONE, + var metricsOptions: CacheMetrics? = null, + var letterSpacingOptions: Double? = null, + var fontFeatures: FontFeatures = FontFeatures(), +) @@ -0,0 +1,6 @@ +package com.dropbear.ui.styling.fonts + +class FontFeature( + val tag: FeatureTag, + val value: UInt, +) @@ -0,0 +1,15 @@ +package com.dropbear.ui.styling.fonts + +class FontFeatures(val features: MutableList<FontFeature> = mutableListOf()) { + fun set(tag: FeatureTag, value: UInt) { + features.add(FontFeature(tag, value)) + } + + fun enable(tag: FeatureTag) { + set(tag, 1u) + } + + fun disable(tag: FeatureTag) { + set(tag, 0u) + } +} @@ -0,0 +1,7 @@ +package com.dropbear.ui.styling.fonts + +enum class FontStyle { + Normal, + Italic, + Oblique, +} @@ -0,0 +1,15 @@ +package com.dropbear.ui.styling.fonts + +class FontWeight(val value: Int) { + companion object { + val THIN = FontWeight(100) + val EXTRA_LIGHT = FontWeight(200) + val LIGHT = FontWeight(300) + val NORMAL = FontWeight(400) + val MEDIUM = FontWeight(500) + val SEMIBOLD = FontWeight(600) + val BOLD = FontWeight(700) + val EXTRA_BOLD = FontWeight(800) + val BLACK = FontWeight(900) + } +} @@ -0,0 +1,13 @@ +package com.dropbear.ui.styling.fonts + +enum class Stretch { + UltraCondensed, + ExtraCondensed, + Condensed, + SemiCondensed, + Normal, + SemiExpanded, + Expanded, + ExtraExpanded, + UltraExpanded, +} @@ -4,14 +4,17 @@ import com.dropbear.ui.UIBuilder import com.dropbear.ui.UIInstruction import com.dropbear.ui.Widget import com.dropbear.ui.WidgetId +import com.dropbear.ui.styling.Alignment +import com.dropbear.ui.styling.BorderRadius import com.dropbear.ui.styling.DynamicButtonStyle import com.dropbear.ui.styling.Padding import com.dropbear.utils.Colour class Button( var text: String, + var alignment: Alignment, var padding: Padding, - var borderRadius: Double, + var borderRadius: BorderRadius, var style: DynamicButtonStyle, var hoverStyle: DynamicButtonStyle, var downStyle: DynamicButtonStyle, @@ -28,8 +31,9 @@ class Button( fun styled(text: String) : Button { val result = Button( text = text, + alignment = Alignment.CENTER, padding = Padding.balanced(20.0, 10.0), - borderRadius = 6.0, + borderRadius = BorderRadius.uniform(6.0), style = DynamicButtonStyle(fill = Colour.BACKGROUND_3), hoverStyle = DynamicButtonStyle(fill = Colour.BACKGROUND_3.adjust(1.2)), downStyle = DynamicButtonStyle(fill = Colour.BACKGROUND_3.adjust(0.8)), @@ -43,8 +47,9 @@ class Button( fun unstyled(text: String) : Button { val result = Button( text = text, + alignment = Alignment.CENTER, padding = Padding.zero(), - borderRadius = 0.0, + borderRadius = BorderRadius(), style = DynamicButtonStyle(), hoverStyle = DynamicButtonStyle(), downStyle = DynamicButtonStyle(),