tirbofish/dropbear · diff
feature: created a checkbox refactor: changed up my UI structure Unverified
@@ -80,6 +80,14 @@ pub struct Camera { pub view_mat: DMat4, /// Projection Matrix pub proj_mat: DMat4, + + /// Smooth damping factor for camera position updates (0.0 = no smoothing, higher = more smoothing) + /// This helps prevent jittering when camera follows physics-driven objects + pub position_smoothing: f64, + /// Internal: previous eye position for smoothing + smooth_eye: DVec3, + /// Internal: previous target position for smoothing + smooth_target: DVec3, } /// A simple builder/struct that allows you to build a [`Camera`] @@ -165,6 +173,9 @@ impl Camera { }, view_mat, proj_mat, + position_smoothing: 10.0, // Default smoothing value + smooth_eye: builder.eye, + smooth_target: builder.target, }; log::debug!("Created new camera{}", if let Some(l) = label { format!(" with the label {}", l) } else { String::new() } ); @@ -217,7 +228,17 @@ impl Camera { } fn build_vp(&mut self) -> DMat4 { - let view = DMat4::look_at_lh(self.eye, self.target, self.up); + // Apply smooth interpolation to camera position if smoothing is enabled + if self.position_smoothing > 0.0 { + let lerp_factor = (1.0 / (1.0 + self.position_smoothing)).clamp(0.0, 1.0); + self.smooth_eye = self.smooth_eye.lerp(self.eye, lerp_factor); + self.smooth_target = self.smooth_target.lerp(self.target, lerp_factor); + } else { + self.smooth_eye = self.eye; + self.smooth_target = self.target; + } + + let view = DMat4::look_at_lh(self.smooth_eye, self.smooth_target, self.up); let proj = DMat4::perspective_infinite_reverse_lh( self.settings.fov_y.to_radians(), self.aspect, @@ -23,12 +23,14 @@ use dropbear_macro::SerializableComponent; pub struct EntityTransform { local: Transform, world: Transform, + #[serde(skip)] + previous_world: Option<Transform>, } impl EntityTransform { /// Creates a new [EntityTransform] from a local and world [Transform] pub fn new(local: Transform, world: Transform) -> Self { - Self { local, world } + Self { local, world, previous_world: None } } /// Creates a new [EntityTransform] from a world [Transform] and a default local transform. @@ -38,6 +40,7 @@ impl EntityTransform { Self { world, local: Transform::default(), + previous_world: None, } } @@ -74,6 +77,26 @@ impl EntityTransform { scale: self.world.scale * self.local.scale, } } + + /// Returns an interpolated transform between previous and current world transforms + /// This is used to smooth out rendering between physics updates + pub fn interpolate(&self, alpha: f64) -> Transform { + if let Some(prev) = self.previous_world { + let current = self.sync(); + Transform { + position: prev.position.lerp(current.position, alpha), + rotation: prev.rotation.slerp(current.rotation, alpha), + scale: prev.scale.lerp(current.scale, alpha), + } + } else { + self.sync() + } + } + + /// Stores the current world transform as the previous state for interpolation + pub fn store_previous(&mut self) { + self.previous_world = Some(self.world); + } } /// A type that represents a position, rotation and scale of an entity @@ -1,21 +1,26 @@ mod button; mod utils; mod text; +mod align; +mod checkbox; +use std::any::Any; use std::cell::RefCell; use std::collections::HashMap; +use std::fmt::Debug; use ::jni::JNIEnv; use ::jni::objects::JObject; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use yakui::{Alignment, MainAxisSize, Yakui}; -use yakui::font::Fonts; use dropbear_engine::utils::ResourceReference; use dropbear_macro::SerializableComponent; use dropbear_traits::SerializableComponent; use crate::scripting::jni::utils::{FromJObject}; use crate::scripting::result::DropbearNativeResult; +use crate::ui::align::{AlignParser}; use crate::ui::button::ButtonParser; +use crate::ui::checkbox::CheckboxParser; use crate::ui::text::TextParser; thread_local! { @@ -38,32 +43,17 @@ pub struct UIComponent { pub struct WidgetState { pub clicked: bool, pub hovering: bool, -} - -pub trait NativeWidget: Send + std::fmt::Debug { - fn build(self: Box<Self>, states: &mut HashMap<i64, WidgetState>); -} - -#[derive(Debug)] -pub struct WrapperWidget<T> { - pub id: i64, - pub widget: T, + pub checked: bool, } pub trait WidgetParser: Send + Sync { - fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<Box<dyn NativeWidget>>>; + fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>>; fn name(&self) -> String; } -pub fn register_widget_parser<P: WidgetParser + 'static>(parser: P) { - UI_CONTEXT.with(|v| { - v.borrow().parsers.lock().push(Box::new(parser)); - }); -} - pub struct UiContext { pub yakui_state: Mutex<Yakui>, - pub instruction_set: Mutex<Vec<Box<dyn NativeWidget>>>, + pub instruction_set: Mutex<Vec<UiInstructionType>>, pub widget_states: Mutex<HashMap<i64, WidgetState>>, pub parsers: Mutex<Vec<Box<dyn WidgetParser>>>, } @@ -75,34 +65,127 @@ pub fn poll() { let mut widget_states = ctx.widget_states.lock(); widget_states.clear(); - - let current_instructions = instructions.drain(..).collect::<Vec<Box<dyn NativeWidget>>>(); + + let current_instructions = instructions.drain(..).collect::<Vec<UiInstructionType>>(); + + let tree = build_tree(current_instructions); + yakui::widgets::Align::new(Alignment::TOP_LEFT).show(|| { yakui::widgets::List::column() - .main_axis_size(MainAxisSize::Min) + .main_axis_size(MainAxisSize::Max) .show(|| { - for i in current_instructions { - i.build(&mut widget_states); - } + render_tree(tree, &mut widget_states); }); }); }); } +fn build_tree(instructions: Vec<UiInstructionType>) -> Vec<UiNode> { + let mut stack: Vec<UiNode> = Vec::new(); + let mut root = Vec::new(); + + for instruction in instructions { + match &instruction { + UiInstructionType::Containered(container_ty) => { + match container_ty { + ContaineredWidgetType::Start { .. } => { + stack.push(UiNode { + instruction, + children: Vec::new(), + }); + } + ContaineredWidgetType::End { .. } => { + if let Some(node) = stack.pop() { + if let Some(parent) = stack.last_mut() { + parent.children.push(node); + } else { + root.push(node); + } + } + } + } + } + UiInstructionType::Widget(_) => { + let node = UiNode { + instruction, + children: Vec::new(), + }; + + if let Some(parent) = stack.last_mut() { + parent.children.push(node); + } else { + root.push(node); + } + } + } + } + + root +} + +pub fn render_tree(nodes: Vec<UiNode>, widget_state: &mut HashMap<i64, WidgetState>) { + for node in nodes { + match node.instruction { + UiInstructionType::Containered(container_ty) => { + match container_ty { + ContaineredWidgetType::Start { widget, .. } => { + widget.render(node.children, widget_state); + } + ContaineredWidgetType::End { .. } => { + // already handled in tree building + } + } + } + UiInstructionType::Widget(widget) => { + widget.render(widget_state); + } + } + } +} + +#[derive(Debug)] +pub enum UiInstructionType { + Containered(ContaineredWidgetType), + Widget(Box<dyn NativeWidget>), +} + +#[derive(Debug)] +pub enum ContaineredWidgetType { + Start { + id: i64, + widget: Box<dyn ContaineredWidget>, + }, + End { + id: i64, + } +} + +pub trait NativeWidget: Send + Sync + Debug { + fn render(self: Box<Self>, state: &mut HashMap<i64, WidgetState>); + fn id(&self) -> i64; + fn as_any(&self) -> &dyn Any; +} + +pub trait ContaineredWidget: Send + Sync + Debug { + fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut HashMap<i64, WidgetState>); + fn as_any(&self) -> &dyn Any; +} + +pub struct UiNode { + pub instruction: UiInstructionType, + pub children: Vec<UiNode>, +} + impl UiContext { pub fn new() -> Self { let mut parsers: Vec<Box<dyn WidgetParser>> = Vec::new(); parsers.push(Box::new(ButtonParser)); parsers.push(Box::new(TextParser)); + parsers.push(Box::new(AlignParser)); + parsers.push(Box::new(CheckboxParser)); let yakui = Yakui::new(); - let fonts = yakui.dom().get_global_or_init(Fonts::default); - fonts.set_sans_serif_family("Roboto"); - fonts.set_serif_family("Roboto"); - fonts.set_cursive_family("Roboto"); - fonts.set_fantasy_family("Roboto"); - fonts.set_monospace_family("Roboto"); Self { yakui_state: Mutex::new(yakui), @@ -123,7 +206,7 @@ pub trait UiWidgetType: FromJObject { pub mod jni { #![allow(non_snake_case)] - use jni::sys::{jboolean, jlong}; + use jni::sys::{jlong}; use jni::objects::{JClass, JObjectArray}; use jni::JNIEnv; use crate::convert_ptr; @@ -165,30 +248,8 @@ pub mod jni { } } - ui.instruction_set.lock().extend(rust_instructions); - } - - #[unsafe(no_mangle)] - pub extern "system" fn Java_com_dropbear_ui_widgets_ButtonNative_getClicked( - _env: JNIEnv, - _class: JClass, - ui_buf_ptr: jlong, - id: jlong, - ) -> jboolean { - let ui = convert_ptr!(ui_buf_ptr => UiContext); - let states = ui.widget_states.lock(); - states.get(&id).map(|s| s.clicked).unwrap_or(false).into() - } - - #[unsafe(no_mangle)] - pub extern "system" fn Java_com_dropbear_ui_widgets_ButtonNative_getHovering( - _env: JNIEnv, - _class: JClass, - ui_buf_ptr: jlong, - id: jlong, - ) -> jboolean { - let ui = convert_ptr!(ui_buf_ptr => UiContext); - let states = ui.widget_states.lock(); - states.get(&id).map(|s| s.hovering).unwrap_or(false).into() + let mut instruction_set = ui.instruction_set.lock(); + instruction_set.clear(); + instruction_set.extend(rust_instructions); } } @@ -0,0 +1,81 @@ +use std::any::Any; +use std::collections::HashMap; +use jni::JNIEnv; +use jni::objects::JObject; +use yakui::Alignment; +use crate::scripting::jni::utils::FromJObject; +use crate::scripting::result::DropbearNativeResult; +use crate::ui::{ContaineredWidget, ContaineredWidgetType, UiInstructionType, UiNode, WidgetParser, WidgetState}; + +pub(crate) struct AlignParser; + +#[derive(Debug, Clone)] +pub(crate) struct AlignWidget { + pub alignment: Alignment, +} + +impl WidgetParser for AlignParser { + fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> { + 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("AlignmentInstruction$StartAlignmentBlock") { + let align_obj = env.get_field(obj, "align", "Lcom/dropbear/ui/widgets/Align;")?.l()?; + let align = yakui::widgets::Align::from_jobject(env, &align_obj)?; + + let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?; + let id = env.get_field(id_obj, "id", "J")?.j()?; + + return Ok(Some(UiInstructionType::Containered( + ContaineredWidgetType::Start { + id, + widget: Box::new(AlignWidget { + alignment: align.alignment, + }), + } + ))); + } + + if name_string.contains("AlignmentInstruction$EndAlignmentBlock") { + let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?; + let id = env.get_field(id_obj, "id", "J")?.j()?; + + return Ok(Some(UiInstructionType::Containered( + ContaineredWidgetType::End { id } + ))); + } + + Ok(None) + } + + fn name(&self) -> String { + String::from("AlignParser") + } +} + +impl ContaineredWidget for AlignWidget { + fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut HashMap<i64, WidgetState>) { + yakui::widgets::Align::new(self.alignment).show(|| { + super::render_tree(children, state); + }); + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl FromJObject for yakui::widgets::Align { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> + where + Self: Sized + { + let align_obj = env.get_field(obj, "align", "Lcom/dropbear/ui/styling/Alignment;")?.l()?; + let alignment = Alignment::from_jobject(env, &align_obj)?; + + Ok(Self { + alignment, + }) + } +} @@ -1,33 +1,39 @@ -use jni::JNIEnv; -use jni::objects::JObject; +use std::any::Any; +use std::collections::HashMap; 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; -use std::collections::HashMap; -use crate::ui::{NativeWidget, WidgetParser, WidgetState, WrapperWidget}; +use ::jni::JNIEnv; +use ::jni::objects::JObject; +use crate::ui::{NativeWidget, UiInstructionType, WidgetParser, WidgetState}; pub(crate) struct ButtonParser; +#[derive(Debug)] +pub(crate) struct ButtonWidget { + pub id: i64, + pub button: yakui::widgets::Button, +} + impl WidgetParser for ButtonParser { - fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<Box<dyn NativeWidget>>> { + fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> { 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(); - // println!("ButtonParser obj get_string result: {}", name_string); 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)?; + let button = yakui::widgets::Button::from_jobject(env, &button_obj)?; let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?; let id = env.get_field(id_obj, "id", "J")?.j()?; - return Ok(Some(Box::new(WrapperWidget { + return Ok(Some(UiInstructionType::Widget(Box::new(ButtonWidget { id, - widget: btn, - }))); + button, + })))); } Ok(None) @@ -38,14 +44,23 @@ impl WidgetParser for ButtonParser { } } -impl NativeWidget for WrapperWidget<yakui::widgets::Button> { - fn build(self: Box<Self>, states: &mut HashMap<i64, WidgetState>) { - let res = self.widget.show(); +impl NativeWidget for ButtonWidget { + fn render(self: Box<Self>, states: &mut HashMap<i64, WidgetState>) { + let res = self.button.show(); states.insert(self.id, WidgetState { clicked: res.clicked, hovering: res.hovering, + checked: false, // always be false because it is not a checkbox, obv }); } + + fn id(&self) -> i64 { + self.id + } + + fn as_any(&self) -> &dyn Any { + self + } } impl FromJObject for Button { @@ -55,40 +70,22 @@ impl FromJObject for Button { let text: String = env.get_string(&text_jstring)?.into(); let f = env.get_field(obj, "alignment", "Lcom/dropbear/ui/styling/Alignment;")?.l()?; - let alignment = Alignment::from_jobject( - env, - &f - )?; + let alignment = Alignment::from_jobject(env, &f)?; let f = env.get_field(obj, "padding", "Lcom/dropbear/ui/styling/Padding;")?.l()?; - let padding = Pad::from_jobject( - env, - &f - )?; + let padding = Pad::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 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 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 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 - )?; + let down_style = DynamicButtonStyle::from_jobject(env, &f)?; Ok(Self { text: Cow::Owned(text), @@ -101,3 +98,37 @@ impl FromJObject for Button { }) } } + +pub mod jni { + #![allow(non_snake_case)] + + use jni::JNIEnv; + use jni::objects::JClass; + use jni::sys::{jboolean, jlong}; + use crate::convert_ptr; + use crate::ui::UiContext; + + #[unsafe(no_mangle)] + pub extern "system" fn Java_com_dropbear_ui_widgets_ButtonNative_getClicked( + _env: JNIEnv, + _class: JClass, + ui_buf_ptr: jlong, + id: jlong, + ) -> jboolean { + let ui = convert_ptr!(ui_buf_ptr => UiContext); + let states = ui.widget_states.lock(); + states.get(&id).map(|s| s.clicked).unwrap_or(false).into() + } + + #[unsafe(no_mangle)] + pub extern "system" fn Java_com_dropbear_ui_widgets_ButtonNative_getHovering( + _env: JNIEnv, + _class: JClass, + ui_buf_ptr: jlong, + id: jlong, + ) -> jboolean { + let ui = convert_ptr!(ui_buf_ptr => UiContext); + let states = ui.widget_states.lock(); + states.get(&id).map(|s| s.hovering).unwrap_or(false).into() + } +} @@ -0,0 +1,99 @@ +use std::any::Any; +use std::collections::HashMap; +use ::jni::JNIEnv; +use ::jni::objects::JObject; +use yakui::widgets::Checkbox; +use crate::scripting::result::DropbearNativeResult; +use crate::ui::{NativeWidget, UiInstructionType, WidgetParser, WidgetState}; + +pub(crate) struct CheckboxParser; + +#[derive(Debug)] +pub(crate) struct CheckboxWidget { + pub id: i64, + pub checkbox: Checkbox, +} + +impl WidgetParser for CheckboxParser { + fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> { + 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("CheckboxInstruction$Checkbox") { + let checked = env.get_field(obj, "checked", "Z")?.z()?; + let checkbox = Checkbox::new(checked); + + let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?; + let id = env.get_field(id_obj, "id", "J")?.j()?; + + return Ok(Some(UiInstructionType::Widget(Box::new(CheckboxWidget { + id, + checkbox, + })))); + } + + Ok(None) + } + + fn name(&self) -> String { + String::from("CheckboxParser") + } +} + +impl NativeWidget for CheckboxWidget { + fn render(self: Box<Self>, state: &mut HashMap<i64, WidgetState>) { + let resp = self.checkbox.show(); + state.insert(self.id, WidgetState { + clicked: false, + hovering: false, + checked: resp.checked, + }); + + } + + fn id(&self) -> i64 { + self.id + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +pub mod jni { + #![allow(non_snake_case)] + + use jni::JNIEnv; + use jni::objects::JClass; + use jni::sys::{jboolean, jlong}; + use crate::convert_ptr; + use crate::ui::UiContext; + + #[unsafe(no_mangle)] + pub extern "system" fn Java_com_dropbear_ui_widgets_CheckboxNative_getChecked( + _env: JNIEnv, + _: JClass, + ui_buf_ptr: jlong, + id: jlong, + ) -> jboolean { + let ui = convert_ptr!(ui_buf_ptr => UiContext); + + if let Some(v) = ui.widget_states.lock().get(&(id as i64)) { + return v.checked.into(); + } + false.into() + } + + #[unsafe(no_mangle)] + pub extern "system" fn Java_com_dropbear_ui_widgets_CheckboxNative_hasCheckedState( + _env: JNIEnv, + _: JClass, + ui_buf_ptr: jlong, + id: jlong, + ) -> jboolean { + let ui = convert_ptr!(ui_buf_ptr => UiContext); + + ui.widget_states.lock().contains_key(&(id as i64)).into() + } +} @@ -1,3 +1,4 @@ +use std::any::Any; use std::borrow::Cow; use std::collections::HashMap; use jni::JNIEnv; @@ -6,12 +7,18 @@ use yakui::style::TextStyle; use yakui::widgets::Pad; use crate::scripting::jni::utils::FromJObject; use crate::scripting::result::DropbearNativeResult; -use crate::ui::{NativeWidget, WidgetParser, WidgetState, WrapperWidget}; +use crate::ui::{NativeWidget, UiInstructionType, WidgetParser, WidgetState}; pub(crate) struct TextParser; +#[derive(Debug)] +pub(crate) struct TextWidget { + pub id: i64, + pub text: yakui::widgets::Text, +} + impl WidgetParser for TextParser { - fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<Box<dyn NativeWidget>>> { + fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> { 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(); @@ -24,10 +31,10 @@ impl WidgetParser for TextParser { let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?; let id = env.get_field(id_obj, "id", "J")?.j()?; - return Ok(Some(Box::new(WrapperWidget { + return Ok(Some(UiInstructionType::Widget(Box::new(TextWidget { id, - widget: text, - }))); + text, + })))) } Ok(None) @@ -38,9 +45,17 @@ impl WidgetParser for TextParser { } } -impl NativeWidget for WrapperWidget<yakui::widgets::Text> { - fn build(self: Box<Self>, _states: &mut HashMap<i64, WidgetState>) { - self.widget.show(); +impl NativeWidget for TextWidget { + fn render(self: Box<Self>, _state: &mut HashMap<i64, WidgetState>) { + let _ = self.text.show(); // no response + } + + fn id(&self) -> i64 { + self.id + } + + fn as_any(&self) -> &dyn Any { + self } } @@ -24,7 +24,6 @@ use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, PhysicsStatePtr, Wor use eucalyptus_core::command::COMMAND_BUFFER; use eucalyptus_core::scene::loading::IsSceneLoaded; use std::collections::HashMap; -use std::env::current_exe; use std::path::PathBuf; use wgpu::SurfaceConfiguration; use winit::window::Fullscreen; @@ -81,7 +80,7 @@ fn find_jvm_library_path() -> PathBuf { fn find_jvm_library_path() -> PathBuf { let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None; - for entry in std::fs::read_dir(current_exe().unwrap().parent().unwrap()).expect("Unable to read directory") { + for entry in std::fs::read_dir(std::env::current_exe().unwrap().parent().unwrap()).expect("Unable to read directory") { let entry = entry.expect("Unable to get directory entry"); let path = entry.path(); @@ -105,6 +104,7 @@ fn find_jvm_library_path() -> PathBuf { .map(|(path, _)| path) .expect("No suitable candidate for a JVM targeted play mode session available") } + pub struct PlayMode { scene_command: SceneCommand, input_state: InputState, @@ -186,6 +186,11 @@ impl Scene for PlayMode { } } + // Store previous transform states for interpolation + for transform in self.world.query::<&mut EntityTransform>().iter() { + transform.store_previous(); + } + let mut sync_updates = Vec::new(); for (entity, label, _) in self.world.query::<(Entity, &Label, &EntityTransform)>().iter() { @@ -5,5 +5,16 @@ package com.dropbear.ui * describing a widget such as a button, or even a column. */ abstract class Widget { + /** + * The [WidgetId] used to differentiate between two different widgets. + * + * It is typically derived from the name of the widget. + */ abstract val id: WidgetId + + /** + * Converts the [Widget] to a [UIInstruction] list, allowing to be added + * by a [UIBuilder]. + */ + abstract fun toInstruction(): List<UIInstruction> } @@ -2,4 +2,6 @@ package com.dropbear.ui import com.dropbear.utils.ID -class WidgetId(val id: Long) : ID(id) +class WidgetId(val id: Long) : ID(id) { + constructor(id: String) : this(id.hashCode().toLong()) +} @@ -7,32 +7,42 @@ import com.dropbear.ui.WidgetId import com.dropbear.ui.styling.Alignment class Align( + override var id: WidgetId, var align: Alignment, -): Widget() { - override lateinit var id: WidgetId - + var widgets: MutableList<Widget> = mutableListOf(), +): Widget() { companion object { - fun center() = Align(align = Alignment.CENTER) + fun center(id: WidgetId) = Align(align = Alignment.CENTER, id = id) } sealed class AlignmentInstruction: UIInstruction { - data class Center(val id: WidgetId, val align: Align) : AlignmentInstruction() + data class StartAlignmentBlock(val id: WidgetId, val align: Align) : AlignmentInstruction() + data class EndAlignmentBlock(val id: WidgetId) : AlignmentInstruction() } - fun toInstruction(): AlignmentInstruction.Center { - return AlignmentInstruction.Center(this.id, this) + override fun toInstruction(): List<UIInstruction> { + val instructions = mutableListOf<UIInstruction>() + instructions.add(AlignmentInstruction.StartAlignmentBlock(id, this)) + widgets.forEach { widget -> + widget.toInstruction().forEach { instruction -> + instructions.add(instruction) + } + } + instructions.add(AlignmentInstruction.EndAlignmentBlock(id)) + return instructions.toList() } } - -fun UIBuilder.center(block: UIBuilder.() -> Unit = {}) { - val align = Align.center() - instructions.add(align.toInstruction()) - block() +fun UIBuilder.center(id: WidgetId, block: UIBuilder.() -> Unit = {}) { + val align = Align.center(id) + instructions.add(Align.AlignmentInstruction.StartAlignmentBlock(align.id, align)) + block(this) + instructions.add(Align.AlignmentInstruction.EndAlignmentBlock(align.id)) } -fun UIBuilder.align(alignment: Alignment, block: UIBuilder.() -> Unit = {}) { - val align = Align(alignment) - instructions.add(align.toInstruction()) - block() +fun UIBuilder.align(alignment: Alignment, id: WidgetId, block: UIBuilder.() -> Unit = {}) { + val align = Align(align = alignment, id = id) + instructions.add(Align.AlignmentInstruction.StartAlignmentBlock(align.id, align)) + block(this) + instructions.add(Align.AlignmentInstruction.EndAlignmentBlock(align.id)) } @@ -96,8 +96,8 @@ class Button( data class Button(val id: WidgetId, val button: com.dropbear.ui.widgets.Button) : ButtonInstruction() } - fun toInstruction(): ButtonInstruction.Button { - return ButtonInstruction.Button(this.id, this) + override fun toInstruction(): List<UIInstruction> { + return listOf(ButtonInstruction.Button(this.id, this)) } } @@ -107,6 +107,8 @@ expect fun Button.getHovering(): Boolean // fits that of yakui_widgets::shorthand::button fun UIBuilder.button(text: String, block: Button.() -> Unit = {}): Button { val btn = Button.styled(text).apply(block) - instructions.add(btn.toInstruction()) + btn.toInstruction().forEach { + instructions.add(it) + } return btn } @@ -0,0 +1,48 @@ +package com.dropbear.ui.widgets + +import com.dropbear.ui.UIBuilder +import com.dropbear.ui.UIInstruction +import com.dropbear.ui.Widget +import com.dropbear.ui.WidgetId + +class Checkbox( + override val id: WidgetId, + initiallyChecked: Boolean, +) : Widget() { + private var cachedChecked: Boolean = initiallyChecked + + /** + * To set a new checkbox value, make it so the class is different next frame. + */ + var checked: Boolean + get() { + if (hasCheckedState()) { + cachedChecked = getChecked() + } + return cachedChecked + } + private set(value) { + cachedChecked = value + } + + init { + checked = initiallyChecked + } + + sealed class CheckboxInstruction: UIInstruction { + data class Checkbox(val id: WidgetId, val checked: Boolean) : CheckboxInstruction() + } + + override fun toInstruction(): List<UIInstruction> { + return listOf(CheckboxInstruction.Checkbox(id, checked)) + } +} + +expect fun Checkbox.getChecked(): Boolean +expect fun Checkbox.hasCheckedState(): Boolean + +fun UIBuilder.checkbox(checked: Boolean, id: WidgetId, block: Checkbox.() -> Unit = {}): Checkbox { + val cb = Checkbox(id, checked).apply(block) + cb.toInstruction().forEach { instruction -> instructions.add(instruction) } + return cb +} @@ -0,0 +1,69 @@ +package com.dropbear.ui.widgets + +import com.dropbear.math.Vector2d +import com.dropbear.ui.UIBuilder +import com.dropbear.ui.UIInstruction +import com.dropbear.ui.Widget +import com.dropbear.ui.WidgetId +import com.dropbear.ui.buildUI +import com.dropbear.utils.Colour + +class ColouredBox( + override val id: WidgetId, + var colour: Colour = Colour.WHITE, + var minSize: Vector2d = Vector2d.zero(), +) : Widget() { + var widgets: MutableList<UIInstruction> = mutableListOf() + + companion object { + fun sized(id: WidgetId, colour: Colour, size: Vector2d): ColouredBox { + return ColouredBox(id, colour, size) + } + + fun container(id: WidgetId, colour: Colour): ColouredBox { + return ColouredBox(id, colour, Vector2d.zero()) + } + } + + sealed class ColouredBoxInstruction: UIInstruction { + data class StartColouredBoxInstruction(val id: WidgetId, val box: ColouredBox): ColouredBoxInstruction() + data class EndColouredBoxInstruction(val id: WidgetId): ColouredBoxInstruction() + } + + override fun toInstruction(): List<UIInstruction> { + val list = mutableListOf<UIInstruction>() + list.add(ColouredBoxInstruction.StartColouredBoxInstruction(this.id, this)) + list.addAll(widgets) + list.add(ColouredBoxInstruction.EndColouredBoxInstruction(this.id)) + return list.toList() + } + + fun addChild(widget: Widget) { + widgets.addAll(widget.toInstruction()) + } +} + +fun UIBuilder.colouredBox(id: WidgetId, colour: Colour, minSize: Vector2d, block: ColouredBox.() -> Unit = {}): ColouredBox { + val box = ColouredBox(id, colour, minSize).apply(block) + instructions.addAll(box.toInstruction()) + return box +} + +fun UIBuilder.colouredBoxContainer(id: WidgetId, colour: Colour, children: UIBuilder.() -> Unit): ColouredBox { + val box = ColouredBox(id = id, colour = colour) + val newUI = UIBuilder() + children(newUI) // execute + box.widgets.addAll(newUI.build()) + instructions.addAll(box.toInstruction()) + return box +} + +fun dummy() { + val instructions = buildUI { + colouredBox(WidgetId("something"), Colour.TRANSPARENT, Vector2d.zero()) + + colouredBoxContainer(WidgetId("something"), Colour.TRANSPARENT) { + + } + } +} @@ -37,8 +37,8 @@ class Text( data class Text(val id: WidgetId, val text: com.dropbear.ui.widgets.Text) : TextInstruction() } - fun toInstruction(): TextInstruction.Text { - return TextInstruction.Text(this.id, this) + override fun toInstruction(): List<UIInstruction> { + return listOf(TextInstruction.Text(this.id, this)) } } @@ -49,7 +49,9 @@ fun UIBuilder.label(text: String, block: Text.() -> Unit = {}): Text { style = style, padding = Padding.zero() ).apply(block) - instructions.add(text.toInstruction()) + text.toInstruction().forEach { + instructions.add(it) + } return text } @@ -61,6 +63,8 @@ fun UIBuilder.text(size: Double, text: String, block: Text.() -> Unit = {}): Tex style = style, padding = Padding.zero() ).apply(block) - instructions.add(text.toInstruction()) + text.toInstruction().forEach { + instructions.add(it) + } return text } @@ -0,0 +1,12 @@ +package com.dropbear.ui.widgets; + +import com.dropbear.EucalyptusCoreLoader; + +public class CheckboxNative { + static { + new EucalyptusCoreLoader().ensureLoaded(); + } + + public static native boolean getChecked(long uiBufferHandle, long id); + public static native boolean hasCheckedState(long uiBufferHandle, long id); +} @@ -0,0 +1,11 @@ +package com.dropbear.ui.widgets + +import com.dropbear.DropbearEngine + +actual fun Checkbox.getChecked(): Boolean { + return CheckboxNative.getChecked(DropbearEngine.native.uiBufferHandle, id.id) +} + +actual fun Checkbox.hasCheckedState(): Boolean { + return CheckboxNative.hasCheckedState(DropbearEngine.native.uiBufferHandle, id.id) +} @@ -0,0 +1,9 @@ +package com.dropbear.ui.widgets + +actual fun Checkbox.getChecked(): Boolean { + TODO("Not yet implemented") +} + +actual fun Checkbox.hasCheckedState(): Boolean { + return false +}