tirbofish/dropbear · commit
2437ae2233ecb565a0d94ad6b0e76d41ec500bb1
wip: got yakui ui to work, and i should be able to create a little "buffer" typa thing. hooray! Unverified
@@ -1,3 +1,3 @@ [target.x86_64-unknown-linux-gnu] linker = "clang" -rustflags = ["-C", "link-arg=-fuse-ld=mold", "-C", "relocation-model=pic"] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] @@ -73,6 +73,8 @@ rapier3d = { version = "0.32", features = [ "simd-stable", "serde-serialize" ] } cbindgen = { version = "0.29.2" } postcard = { version = "1.1"} pollster = "0.4" +yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "ddf7614" } +yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "ddf7614" } [workspace.dependencies.image] version = "0.25" @@ -16,7 +16,6 @@ If you might have not realised, all the crates/projects names are after Australi - [eucalyptus-core](https://github.com/tirbofish/dropbear/tree/main/crates/eucalyptus-core) is the library used by both `redback-runtime` and `eucalyptus-editor` to share configs and metadata between each other. - [redback-runtime](https://github.com/tirbofish/dropbear/tree/main/crates/redback-runtime) is the runtime used to load .eupak files and run the game loaded on them. - [magna-carta](https://github.com/tirbofish/dropbear/tree/main/crates/magna-carta) is a rust library used to generate compile-time Kotlin/Native and Kotlin/JVM metadata for searching. -- [kino-gui](https://github.com/tirbofish/dropbear/tree/main/crates/kino-gui) is a wgpu primary UI renderer used for displaying shapes, text, and images in a simple immediate-mode way. [//]: # (- [eucalyptus-sdk](https://github.com/tirbofish/dropbear/tree/main/eucalyptus-sdk) is used to develop plugins to be used with the `eucalyptus-editor`) @@ -43,6 +43,8 @@ dashmap.workspace = true typetag.workspace = true postcard.workspace = true pollster.workspace = true +yakui-wgpu.workspace = true +yakui.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -31,6 +31,8 @@ pub struct SharedGraphicsContext { pub future_queue: Arc<FutureQueue>, pub supports_storage: bool, pub mipmapper: Arc<MipMapper>, + pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>, + pub yakui_texture: yakui::TextureId, } impl SharedGraphicsContext { @@ -73,6 +75,8 @@ impl SharedGraphicsContext { surface_format: state.surface_format, supports_storage: state.supports_storage, mipmapper: state.mipmapper.clone(), + yakui_renderer: state.yakui_renderer.clone(), + yakui_texture: state.yakui_texture.clone(), } } } @@ -98,6 +98,8 @@ pub struct State { physics_accumulator: Duration, pub scene_manager: scene::Manager, + pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>, + pub yakui_texture: yakui::TextureId, } impl State { @@ -330,6 +332,19 @@ Hardware: let device = Arc::new(device); let queue = Arc::new(queue); + let yakui_renderer = Arc::new(Mutex::new(yakui_wgpu::YakuiWgpu::new( + &device, + &queue + ))); + + let yakui_texture = yakui_renderer.lock().add_texture( + viewport_texture.view.clone(), + wgpu::FilterMode::default(), + wgpu::FilterMode::default(), + wgpu::FilterMode::default(), + wgpu::AddressMode::default(), + ); + let result = Self { surface: Arc::new(surface), surface_format: Texture::TEXTURE_FORMAT, @@ -357,6 +372,8 @@ Hardware: light_cube_bind_group_layout, }), supports_storage: supports_storage_resources, + yakui_renderer, + yakui_texture, }; Ok(result) @@ -10,11 +10,25 @@ use parking_lot::Mutex; use std::collections::HashMap; use std::hash::{DefaultHasher, Hasher}; use std::sync::{Arc, LazyLock}; +use serde::{Deserialize, Serialize}; use wgpu::util::DeviceExt; pub mod cube; // pub mod plane; +#[derive( + Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize +)] +pub enum ProcObj { + /// A parameterized cuboid (box) generated at runtime. + /// + /// Stored as IEEE-754 `f32` bit patterns so the reference remains hashable. + /// Values can be reconstructed with `f32::from_bits`. + /// + /// The `size_bits` represent the full extents (width, height, depth). + Cuboid { size_bits: [u32; 3] }, +} + /// An object that comes with a template, and is generated through parameter input. pub struct ProcedurallyGeneratedObject { pub vertices: Vec<ModelVertex>, @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::path::Path; +use crate::procedural::ProcObj; pub const EUCA_SCHEME: &str = "euca://"; @@ -105,14 +106,10 @@ pub enum ResourceReferenceType { /// The content in bytes. Sometimes, there is a model that is loaded into memory through the /// [`include_bytes!`] macro, this type stores it. Bytes(Vec<u8>), - - /// A parameterized cuboid (box) generated at runtime. - /// - /// Stored as IEEE-754 `f32` bit patterns so the reference remains hashable. - /// Values can be reconstructed with `f32::from_bits`. - /// - /// The `size_bits` represent the full extents (width, height, depth). - Cuboid { size_bits: [u32; 3] }, + + /// An object that can be generated at runtime with the usage of vertices and indices, as well + /// as a solid grey mesh. + ProcObj(ProcObj), } impl Default for ResourceReferenceType { @@ -13,7 +13,6 @@ crate-type = ["rlib", "dylib"] dropbear-traits = { path = "../dropbear-traits" } dropbear-macro = { path = "../dropbear-macro" } magna-carta = { path = "../magna-carta" } -kino-gui = { path = "../kino-gui" } anyhow.workspace = true postcard.workspace = true @@ -44,6 +43,7 @@ semver.workspace = true rustc_version_runtime.workspace = true rapier3d.workspace = true bytemuck.workspace = true +yakui.workspace = true [features] default = [] @@ -28,6 +28,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use crossbeam_channel::Sender; use hecs::Entity; +use dropbear_engine::procedural::ProcObj; use crate::physics::collider::ColliderGroup; use crate::physics::kcc::KCC; use crate::physics::PhysicsState; @@ -185,28 +186,32 @@ impl SceneConfig { .await?; MeshRenderer::from_handle_with_import_scale(loaded, import_scale) } - ResourceReferenceType::Cuboid { size_bits } => { - let size = [ - f32::from_bits(size_bits[0]), - f32::from_bits(size_bits[1]), - f32::from_bits(size_bits[2]), - ]; - log::info!("Loading entity from cuboid: {:?}", size); - { - let mut cache_guard = MODEL_CACHE.lock(); - cache_guard.remove(label); - } + ResourceReferenceType::ProcObj(obj) => { + match obj { + ProcObj::Cuboid { size_bits } => { + let size = [ + f32::from_bits(size_bits[0]), + f32::from_bits(size_bits[1]), + f32::from_bits(size_bits[2]), + ]; + log::info!("Loading entity from cuboid: {:?}", size); + { + let mut cache_guard = MODEL_CACHE.lock(); + cache_guard.remove(label); + } - let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64); - let mut loaded_model = dropbear_engine::procedural::ProcedurallyGeneratedObject::cuboid(size_vec) - .build_model(graphics.clone(), None, Some(label)); + let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64); + let mut loaded_model = dropbear_engine::procedural::ProcedurallyGeneratedObject::cuboid(size_vec) + .build_model(graphics.clone(), None, Some(label)); - let model = loaded_model.make_mut(); - model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits: *size_bits }); + let model = loaded_model.make_mut(); + model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits: *size_bits })); - loaded_model.refresh_registry(); + loaded_model.refresh_registry(); - MeshRenderer::from_handle_with_import_scale(loaded_model, import_scale) + MeshRenderer::from_handle_with_import_scale(loaded_model, import_scale) + } + } } }; @@ -29,7 +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; +use crate::ui::UI_CONTEXT; /// The target of the script. This can be either a JVM or a native library. #[derive(Default, Clone, Debug)] @@ -227,7 +227,9 @@ 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 ui_buf = UI_CONTEXT.with(|v| { + v.as_ptr() + }); let context = DropbearContext { world, @@ -1,191 +1,61 @@ -pub mod rect; - -use once_cell::sync::Lazy; +use std::cell::RefCell; use parking_lot::Mutex; -use kino_gui::prelude::*; -use std::collections::HashMap; - -pub static UI_COMMAND_BUFFER: Lazy<UiContext> = Lazy::new(|| UiContext::new()); - -#[derive(Clone, Copy, Debug)] -pub struct UiResponse { - pub id: u64, - pub was_clicked: bool, -} - -impl UiResponse { - pub fn clicked(&self) -> bool { - self.was_clicked - } +use serde::{Deserialize, Serialize}; +use yakui::Yakui; +use dropbear_engine::utils::ResourceReference; +use dropbear_macro::SerializableComponent; +use dropbear_traits::SerializableComponent; + +thread_local! { + pub static UI_CONTEXT: RefCell<UiContext> = RefCell::new(UiContext::new()); } -pub enum UiCommand { - Rect { - id: u64, - initial: Vector2, - size: Size, - corner_radius: f32, - stroke: f32, - fill: Colour, - stroke_kind: String, - }, - Circle { - id: u64, - center: Vector2, - radius: f32, - fill: Colour, - stroke: f32, - }, +/// A component that can be attached to an entity that renders UI for the entire scene. +/// +/// This UI is used in tandem with a `.kts` (Kotlin Script file) with the dropbear-engine scripting +/// ui DSL. +#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent, Default)] +pub struct UIComponent { + /// Does not render the UI file. + pub disabled: bool, + /// The reference to the script file. + pub ui_file: ResourceReference, } pub struct UiContext { - commands: Mutex<Vec<UiCommand>>, - pub currently_rendering: Mutex<HashMap<u64, UiResponse>>, + pub yakui_state: Mutex<Yakui>, + pub to_render: Mutex<Vec<Box<dyn FnOnce()>>>, } impl UiContext { pub fn new() -> Self { Self { - commands: Mutex::new(Vec::new()), - currently_rendering: Mutex::new(HashMap::new()), + yakui_state: Mutex::new(Yakui::new()), + to_render: Default::default(), } } - - pub fn push(&self, command: UiCommand) { - self.commands.lock().push(command); - } - - pub fn drain_commands(&self) -> Vec<UiCommand> { - self.commands.lock().drain(..).collect() - } - - pub fn update_responses(&self, responses: HashMap<u64, UiResponse>) { - let mut current = self.currently_rendering.lock(); - *current = responses; - } } pub mod jni { - #![allow(non_snake_case)] - - use jni::JNIEnv; - use jni::sys::{jboolean, jlong}; - use jni::objects::JObject; - use kino_gui::prelude::shapes::Rectangle; - use kino_gui::Widget; - use crate::{convert_ptr}; - use crate::scripting::jni::utils::FromJObject; - use crate::ui::{UiCommand, UiContext}; - - #[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); + use jni::sys::jlong; + use crate::convert_ptr; + use crate::ui::{UiContext}; - let rect = match Rectangle::from_jobject(&mut env, &rect) { - Ok(v) => v, - Err(e) => { - let _ = env.throw_new("java/lang/RuntimeException", format!("Unable to convert scripting::Rectangle->kino::Rectangle: {:?}", e)); - return; - } - }; - - ui.push(UiCommand::Rect { - id: rect.id().as_u64(), - initial: rect.initial, - size: rect.size, - corner_radius: 0.0, // rect.corner_radius when available - stroke: 0.0, // rect.stroke when available - fill: rect.fill_colour, - stroke_kind: "Middle".to_string(), // rect.stroke_kind when available - }); - } - #[unsafe(no_mangle)] - pub extern "system" fn Java_com_dropbear_ui_UINative_pushCircle( - mut env: JNIEnv, + pub extern "C" fn Java_YourClass_addOverlay( + _env: jni::JNIEnv, _class: jni::objects::JClass, - ui_buffer_handle: jlong, - circle: JObject, + ui_buf_ptr: jlong, ) { - 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 ui = convert_ptr!(ui_buf_ptr => UiContext); - 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 mut state = ui.to_render.lock(); - 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; - } - }; - - panic!("this is not implemented yet :(") - } - - #[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() - } + state.push(Box::new(move || { + // yakui::colored_box( + // Color::rgba(255, 0, 0, 128), + // yakui::geometry::Vec2::new(width, height) + // ); + })); } } @@ -1,198 +0,0 @@ -use jni::JNIEnv; -use jni::objects::JObject; -use crate::scripting::jni::utils::{FromJObject, ToJObject}; -use crate::scripting::result::DropbearNativeResult; -use crate::scripting::native::DropbearNativeError; -use kino_gui::prelude::*; -use kino_gui::prelude::shapes::Rectangle; -use kino_gui::WidgetId; - -impl FromJObject for Rectangle { - 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); - let fill = Colour { - r: r as f32 / 255.0, - g: g as f32 / 255.0, - b: b as f32 / 255.0, - a: a as f32 / 255.0, - }; - - // 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 - // }; - - let rect = Rectangle::new( - WidgetId::new(id), - [initial_x, initial_y].into(), - [width, height].into(), - fill - ); - - Ok(rect) - } -} - -impl ToJObject for Rectangle { - fn to_jobject<'a>(&self, _env: &mut JNIEnv<'a>) -> DropbearNativeResult<JObject<'a>> { - todo!() - } -} @@ -54,6 +54,7 @@ rfd.workspace = true semver.workspace = true serde.workspace = true + [target.'cfg(unix)'.dependencies] daemonize = "0.5.0" @@ -6,7 +6,7 @@ use dropbear_engine::attenuation::ATTENUATION_PRESETS; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; use dropbear_engine::lighting::LightType; use dropbear_engine::texture::TextureWrapMode; -use dropbear_engine::utils::ResourceReferenceType; +use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; use egui::{CollapsingHeader, ComboBox, DragValue, Grid, RichText, TextEdit, Ui, UiBuilder}; use eucalyptus_core::camera::CameraType; use eucalyptus_core::states::{Camera3D, Light, Property, Script}; @@ -14,7 +14,9 @@ use eucalyptus_core::{fatal, warn}; use glam::{DVec3, Vec3}; use hecs::Entity; use std::time::Instant; +use dropbear_engine::procedural::ProcObj; use eucalyptus_core::properties::{CustomProperties, Value}; +use eucalyptus_core::ui::UIComponent; /// A trait that can added to any component that allows you to inspect the value in the editor. pub trait InspectableComponent { @@ -1008,8 +1010,12 @@ impl InspectableComponent for MeshRenderer { let model_reference = self.model().path.clone(); let model_title = match &model_reference.ref_type { - ResourceReferenceType::Cuboid { .. } => "Cuboid".to_string(), ResourceReferenceType::Unassigned { .. } => "None".to_string(), + ResourceReferenceType::ProcObj(obj) => { + match obj { + ProcObj::Cuboid { .. } => "Cuboid".to_string(), + } + } _ => self.handle().label.clone(), }; @@ -1084,7 +1090,7 @@ impl InspectableComponent for MeshRenderer { if ui .selectable_label( - matches!(model_reference.ref_type, ResourceReferenceType::Cuboid { .. }), + matches!(model_reference.ref_type, ResourceReferenceType::ProcObj(ProcObj::Cuboid { .. })), "Cuboid", ) .clicked() @@ -1127,7 +1133,7 @@ impl InspectableComponent for MeshRenderer { if choose_proc_cuboid { let default_size = match &model_reference.ref_type { - ResourceReferenceType::Cuboid { size_bits } => [ + ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) => [ f32::from_bits(size_bits[0]), f32::from_bits(size_bits[1]), f32::from_bits(size_bits[2]), @@ -1169,7 +1175,7 @@ impl InspectableComponent for MeshRenderer { } } - if let ResourceReferenceType::Cuboid { size_bits } = &self.model().path.ref_type { + if let ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) = &self.model().path.ref_type { let mut size = [ f32::from_bits(size_bits[0]), f32::from_bits(size_bits[1]), @@ -1544,3 +1550,66 @@ impl InspectableComponent for Camera3D { ui.separator(); } } + +impl InspectableComponent for UIComponent { + fn inspect( + &mut self, + _entity: &mut Entity, + cfg: &mut StaticallyKept, + ui: &mut Ui, + _undo_stack: &mut Vec<UndoableAction>, + _signal: &mut Signal, + _label: &mut String, + ) { + ui.vertical(|ui| { + ui.checkbox(&mut self.disabled, "Disable rendering of UI"); + + ui.separator(); + + let (rect, response) = ui.allocate_exact_size( + egui::vec2(ui.available_width(), 72.0), + egui::Sense::click(), + ); + + let fill = if response.hovered() { + ui.visuals().widgets.hovered.bg_fill + } else { + ui.visuals().widgets.inactive.bg_fill + }; + + ui.painter().rect_filled(rect, 4.0, fill); + ui.painter().rect_stroke( + rect, + 4.0, + ui.visuals().widgets.inactive.bg_stroke, + egui::StrokeKind::Inside, + ); + + let mut card_ui = ui.new_child( + UiBuilder::new() + .layout(egui::Layout::centered_and_justified(egui::Direction::LeftToRight)) + .max_rect(rect), + ); + + if let Some(uri) = self.ui_file.as_uri() { + card_ui.label(uri); + } else { + card_ui.label("Drop .kts file here"); + } + + let pointer_released = ui.input(|i| i.pointer.any_released()); + if pointer_released && response.hovered() { + if let Some(asset) = cfg.dragged_asset.clone() { + if let Some(uri) = asset.path.as_uri() { + if uri.ends_with(".kts") { + if let Ok(new_ref) = ResourceReference::from_euca_uri(uri) { + self.ui_file = new_ref; + } + cfg.dragged_asset = None; + } + } + } + } + }); + } +} @@ -4,7 +4,7 @@ use dropbear_engine::entity::{MeshRenderer, Transform}; use dropbear_engine::graphics::{SharedGraphicsContext}; use dropbear_engine::lighting::{Light as EngineLight, LightComponent}; use dropbear_engine::model::{LoadedModel, Material, Model, ModelId, MODEL_CACHE}; -use dropbear_engine::procedural::ProcedurallyGeneratedObject; +use dropbear_engine::procedural::{ProcedurallyGeneratedObject, ProcObj}; use dropbear_engine::texture::{Texture, TextureWrapMode}; use dropbear_engine::utils::{relative_path_from_euca, EUCA_SCHEME, ResourceReference, ResourceReferenceType}; use egui::Align2; @@ -653,7 +653,7 @@ impl SignalController for Editor { .build_model(graphics_clone.clone(), None, Some("Cuboid")); let model = loaded.make_mut(); - model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits }); + model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits })); loaded.refresh_registry(); loaded } else { @@ -692,7 +692,7 @@ impl SignalController for Editor { { let model = loaded_model.make_mut(); - model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits }); + model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits })); for material in &mut model.materials { material.set_tint(graphics_clone.as_ref(), [1.0, 1.0, 1.0, 1.0]); } @@ -775,7 +775,7 @@ impl SignalController for Editor { { let model = loaded_model.make_mut(); - model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits }); + model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits })); } loaded_model.refresh_registry(); @@ -8,6 +8,7 @@ use dropbear_engine::lighting::{Light, LightComponent}; use dropbear_engine::model::{LoadedModel, Material, Model, ModelId}; use dropbear_engine::texture::Texture; use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; +use dropbear_engine::procedural::ProcObj; use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::scene::SceneEntity; pub(crate) use eucalyptus_core::spawn::{PendingSpawnController, PENDING_SPAWNS}; @@ -296,7 +297,7 @@ async fn load_renderer_from_serialized( .build_model(graphics.clone(), None, Some(&label)); let model = loaded_model.make_mut(); - model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits }); + model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits })); loaded_model.refresh_registry(); @@ -313,7 +314,7 @@ async fn load_renderer_from_serialized( .await?; MeshRenderer::from_handle_with_import_scale(loaded, import_scale) } - ResourceReferenceType::Cuboid { size_bits } => { + ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) => { let size = [ f32::from_bits(size_bits[0]), f32::from_bits(size_bits[1]), @@ -324,7 +325,7 @@ async fn load_renderer_from_serialized( .build_model(graphics.clone(), None, Some(&label)); let model = loaded_model.make_mut(); - model.path = ResourceReference::from_reference(ResourceReferenceType::Cuboid { size_bits: *size_bits }); + model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits: *size_bits })); loaded_model.refresh_registry(); @@ -1,13 +0,0 @@ -[package] -name = "kino-gui" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -readme.workspace = true - -[dependencies] -wgpu.workspace = true -anyhow.workspace = true -bytemuck.workspace = true -parking_lot.workspace = true @@ -1,9 +0,0 @@ -# kino-gui - -_kino-gui_ is a 2D UI immediate mode library that is wgpu native and game design oriented. Named after the sap that comes out of a -eucalyptus tree, this library can be used to create 2D designs, create HUDs for a 3D map or create menus for 2D contexts. - -This library is simple to use, and designs itself to be "injected" as an overlay with your surface. - -## Note -It is a heavy WIP, and there are many features still not done yet. @@ -1,77 +0,0 @@ -use std::hash::{DefaultHasher, Hash, Hasher}; -use parking_lot::Mutex; -use crate::math::Size; -use crate::rendering::KinoRenderer; - -pub mod rendering; -pub(crate) mod math; -pub(crate) mod primitives; -pub(crate) mod utils; - -pub mod prelude { - pub use crate::{ - rendering::KinoRenderer, - math::*, - primitives::*, - WidgetId, - Widget, - }; -} - -pub struct GumContext { - screen_size: Size, -} - -impl GumContext { - pub fn new() -> Self { - Self { screen_size: Size::default() } - } -} - -pub struct KinoUICommandBuffer { - contents: Mutex<Vec<Box<dyn Widget>>>, -} - -impl KinoUICommandBuffer { - pub(crate) fn new() -> Self { - Self { - contents: Mutex::new(Vec::new()), - } - } - - /// Drains the [self.contents] and sends back to the renderer - pub(crate) fn process<'a>(&self) -> Vec<Box<dyn Widget>> { - let mut contents = self.contents.lock(); - contents.drain(..).collect() - } - - pub fn add<T: Widget + 'static>(&self, widget: T) { - self.contents.lock().push(Box::new(widget)); - } -} - -pub trait Widget { - fn id(&self) -> WidgetId; - fn draw<'a>(&mut self, renderer: &KinoRenderer, pass: &mut wgpu::RenderPass<'a>); -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] -pub struct WidgetId(u64); - -impl WidgetId { - pub fn new(id: u64) -> Self { - Self(id) - } - - pub fn as_u64(&self) -> u64 { - self.0 - } -} - -impl Into<WidgetId> for String { - fn into(self) -> WidgetId { - let mut hasher = DefaultHasher::new(); - self.hash(&mut hasher); - WidgetId(hasher.finish()) - } -} @@ -1,106 +0,0 @@ -#[derive(Debug, Clone, PartialEq)] -pub struct Rect { - /// States the left most corner of the rectangle in reference to the window - pub initial: Vector2, - /// The width and height that extends from the initial point, with the width extending - /// left and the height extending down. - pub size: Size, -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Vector2 { - pub x: f32, - pub y: f32, -} - -impl Into<Size> for Vector2 { - fn into(self) -> Size { - Size { - width: self.x, - height: self.y, - } - } -} - -impl Into<[f32; 2]> for Vector2 { - fn into(self) -> [f32; 2] { - [self.x, self.y] - } -} - -impl Into<Vector2> for [f32; 2] { - fn into(self) -> Vector2 { - Vector2 { - x: self[0], - y: self[1], - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Size { - pub width: f32, - pub height: f32 -} - -impl Default for Size { - fn default() -> Self { - Self { - // cant be zero otherwise wgpu panics - width: 800.0, - height: 600.0, - } - } -} - -impl Into<Vector2> for Size { - fn into(self) -> Vector2 { - Vector2 { - x: self.width, - y: self.height, - } - } -} - -impl Into<[f32; 2]> for Size { - fn into(self) -> [f32; 2] { - [self.width, self.height] - } -} - -impl Into<Size> for [f32; 2] { - fn into(self) -> Size { - Size { - width: self[0], - height: self[1], - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Colour { - pub r: f32, - pub g: f32, - pub b: f32, - pub a: f32, -} - -pub fn create_orthographic_projection( - left: f32, - right: f32, - bottom: f32, - top: f32, - near: f32, - far: f32, -) -> [[f32; 4]; 4] { - let width = right - left; - let height = top - bottom; - let depth = far - near; - - [ - [2.0 / width, 0.0, 0.0, 0.0], - [0.0, 2.0 / height, 0.0, 0.0], - [0.0, 0.0, -2.0 / depth, 0.0], - [-(right + left) / width, -(bottom + top) / height, -(far + near) / depth, 1.0], - ] -} @@ -1 +0,0 @@ -pub mod shapes; @@ -1,120 +0,0 @@ -use wgpu::RenderPass; -use wgpu::util::DeviceExt; -use crate::math::{create_orthographic_projection, Colour, Size, Vector2}; -use crate::{Widget, WidgetId}; -use crate::rendering::{Globals, KinoRenderer}; -use crate::utils::UniformBuffer; - -pub struct Rectangle { - pub id: WidgetId, - pub initial: Vector2, - pub size: Size, - pub fill_colour: Colour, - - globals_uniform: Option<UniformBuffer<Globals>>, - vertex_buffer: Option<wgpu::Buffer>, -} - -impl Rectangle { - pub fn new(id: WidgetId, initial: Vector2, size: Size, fill_colour: Colour) -> Self { - Self { - id, - initial, - size, - fill_colour, - globals_uniform: None, - vertex_buffer: None, - } - } - - pub(crate) fn create_vertex_buffer(&self, device: &wgpu::Device) -> wgpu::Buffer { - let x = self.initial.x; - let y = self.initial.y; - let w = self.size.width; - let h = self.size.height; - let c = [ - self.fill_colour.r, - self.fill_colour.g, - self.fill_colour.b, - self.fill_colour.a, - ]; - - #[repr(C)] - #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] - struct Vertex { - position: [f32; 3], - fill_colour: [f32; 4], - } - - let vertices: [Vertex; 6] = [ - Vertex { position: [x, y, 0.0], fill_colour: c }, - Vertex { position: [x + w, y, 0.0], fill_colour: c }, - Vertex { position: [x + w, y + h, 0.0], fill_colour: c }, - Vertex { position: [x, y, 0.0], fill_colour: c }, - Vertex { position: [x + w, y + h, 0.0], fill_colour: c }, - Vertex { position: [x, y + h, 0.0], fill_colour: c }, - ]; - - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("rectangle vertex buffer"), - contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX, - }) - } -} - -impl Widget for Rectangle { - fn id(&self) -> WidgetId { - self.id - } - - fn draw<'a>(&mut self, renderer: &KinoRenderer, pass: &mut RenderPass<'a>) { - if self.globals_uniform.is_none() { - let globals = Globals::new( - create_orthographic_projection( - 0.0, - renderer.context.screen_size.width, - renderer.context.screen_size.height, - 0.0, - -1.0, - 1.0 - ), - [ - renderer.context.screen_size.width, - renderer.context.screen_size.height, - ], - ); - - self.globals_uniform = Some(UniformBuffer::new( - &renderer.render.device, - Some(globals), - Some("rectangle globals uniform"), - )); - } - - if self.vertex_buffer.is_none() { - self.vertex_buffer = Some(self.create_vertex_buffer(&renderer.render.device)); - } - - let globals_bind_group = renderer.render.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("rectangle globals bind group"), - layout: &renderer.render.globals_uniform_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: self.globals_uniform.as_ref().unwrap().buffer(), - offset: 0, - size: None, - }), - } - ], - }); - - pass.set_pipeline(&renderer.render.pipeline); - pass.set_bind_group(0, &globals_bind_group, &[]); - pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..)); - - pass.draw(0..6, 0..1); - } -} @@ -1,195 +0,0 @@ -use std::num::NonZeroU64; -use std::sync::Arc; -use anyhow::Context; -use wgpu::{include_wgsl, BindGroupLayoutEntry, ShaderStages, VertexAttribute, VertexStepMode}; -use crate::{GumContext, KinoUICommandBuffer}; -use crate::math::Size; - -pub struct KinoRenderer { - pub(crate) context: GumContext, - pub(crate) render: KinoRenderPipeline, - pub(crate) ui: Arc<KinoUICommandBuffer>, -} - -pub(crate) struct KinoRenderPipeline { - pub(crate) globals_uniform_layout: wgpu::BindGroupLayout, - pub(crate) pipeline: wgpu::RenderPipeline, - pub(crate) device: Arc<wgpu::Device>, - pub(crate) queue: Arc<wgpu::Queue>, -} - -impl KinoRenderer { - pub fn new( - device: Arc<wgpu::Device>, - queue: Arc<wgpu::Queue>, - texture_format: wgpu::TextureFormat, - ) -> anyhow::Result<Self> { - let shader = device.create_shader_module(include_wgsl!("shaders/primitive.wgsl")); - - let globals_uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("kino globals uniform"), - entries: &[ - BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - } - ], - }); - - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("kino render pipeline layout"), - bind_group_layouts: &[ - &globals_uniform_layout - ], - push_constant_ranges: &[], - }); - - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("kino render pipeline"), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - compilation_options: Default::default(), - buffers: &[ - VertexInput::desc() - ], - }, - primitive: Default::default(), - depth_stencil: None, - multisample: Default::default(), - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - compilation_options: Default::default(), - targets: &[ - Some(wgpu::ColorTargetState { - format: texture_format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: Default::default(), - }) - ], - }), - multiview: None, - cache: None, - }); - - Ok(Self { - context: GumContext::new(), - render: KinoRenderPipeline { - globals_uniform_layout, - pipeline, - device, - queue, - }, - ui: Arc::new(KinoUICommandBuffer::new()), - }) - } - - pub fn get_ui(&self) -> Arc<KinoUICommandBuffer> { - self.ui.clone() - } - - /// Uses [self.render] in the backend, creates an encoder with the device provided, and - /// renders the content. - pub fn render_without_encoder( - &mut self, - view: &wgpu::TextureView, - size_to_render: Size, - ) { - let mut encoder = self.render.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("kino render encoder"), - }); - - self.render(&mut encoder, view, size_to_render); - - self.render.queue.submit(std::iter::once(encoder.finish())); - } - - /// Renders the content onto the provided texture/view. - pub fn render( - &mut self, - encoder: &mut wgpu::CommandEncoder, - view: &wgpu::TextureView, - size_to_render: Size, - ) { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("kino render pass"), - color_attachments: &[ - Some(wgpu::RenderPassColorAttachment { - view, - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }) - ], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - self.context.screen_size = size_to_render; - - let mut contents = self.ui.process(); - for mut widget in contents.drain(..) { - widget.draw(&self, &mut pass); - } - } -} - -pub struct VertexInput { - position: [f32; 3], - fill_colour: [f32; 4], -} - -impl VertexInput { - pub fn desc() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: 0, - step_mode: VertexStepMode::Vertex, - attributes: &[ - // position - VertexAttribute { - format: wgpu::VertexFormat::Float32x3, - offset: 0, - shader_location: 0, - }, - - // fill_colour - VertexAttribute { - format: wgpu::VertexFormat::Float32x4, - offset: size_of::<[f32; 3]>() as u64, - shader_location: 1, - } - ], - } - } -} - -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -#[derive(Default)] -pub struct Globals { - pub proj: [[f32; 4]; 4], - pub screen_size: [f32; 2], - pub _padding: [f32; 2], -} - -impl Globals { - pub fn new(proj: [[f32; 4]; 4], screen_size: [f32; 2]) -> Self { - Self { - proj, - screen_size, - _padding: [0.0; 2], - } - } -} @@ -1,30 +0,0 @@ -struct Globals { - proj: mat4x4<f32>, - screen_size: vec2<f32>, -} - -@group(0) @binding(0) -var<uniform> globals: Globals; - -struct VertexInput { - @location(0) position: vec3<f32>, - @location(1) fill_colour: vec4<f32>, -} - -struct VertexOutput { - @builtin(position) position: vec4<f32>, - @location(0) colour: vec4<f32>, -} - -@vertex -fn vs_main(in: VertexInput) -> VertexOutput { - var out: VertexOutput; - out.position = globals.proj * vec4<f32>(in.position, 1.0); - out.colour = in.fill_colour; - return out; -} - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - return in.colour; -} @@ -1,35 +0,0 @@ -use wgpu::util::{BufferInitDescriptor, DeviceExt}; - -pub struct UniformBuffer<T> { - pub inner: T, - buffer: wgpu::Buffer, -} - -impl<T: bytemuck::Pod + Default> UniformBuffer<T> { - pub fn new(device: &wgpu::Device, uniform: Option<T>, label: Option<&str>) -> Self { - let uniform = if let Some(uniform) = uniform { - uniform - } else { - T::default() - }; - - let buffer = device.create_buffer_init(&BufferInitDescriptor { - label, - contents: bytemuck::cast_slice(&[uniform]), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - - Self { - inner: uniform, - buffer, - } - } - - pub fn write(&self, queue: &wgpu::Queue, uniform: T) { - queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[uniform])); - } - - pub fn buffer(&self) -> &wgpu::Buffer { - &self.buffer - } -} @@ -9,7 +9,6 @@ readme = "README.md" [dependencies] dropbear-engine = { path = "../dropbear-engine" } eucalyptus-core = { path = "../eucalyptus-core", features = ["runtime"], default-features = false } -kino-gui = { path = "../kino-gui" } anyhow.workspace = true log.workspace = true @@ -32,6 +31,9 @@ futures.workspace = true glam.workspace = true wgpu.workspace = true egui.workspace = true +yakui.workspace = true +yakui-wgpu.workspace = true +egui_extras.workspace = true [features] debug = [] @@ -5,6 +5,7 @@ use winit::event_loop::ActiveEventLoop; use winit::keyboard::KeyCode; use dropbear_engine::input::{Controller, Keyboard, Mouse}; use crate::PlayMode; +use eucalyptus_core::ui::UI_CONTEXT; impl Keyboard for PlayMode { fn key_down(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { @@ -31,14 +32,57 @@ impl Mouse for PlayMode { self.input_state.mouse_delta = delta; self.input_state.mouse_pos = (position.x, position.y); self.input_state.last_mouse_pos = Some(<(f64, f64)>::from(position)); + + UI_CONTEXT.with(|ctx| { + let yak = ctx.borrow(); + let mut yakui = yak.yakui_state.lock(); + let relative_x = (position.x as f32) - self.viewport_offset.0; + let relative_y = (position.y as f32) - self.viewport_offset.1; + + yakui.handle_event(yakui::event::Event::CursorMoved(Some(yakui::geometry::Vec2::new(relative_x, relative_y)))); + }); } fn mouse_down(&mut self, button: MouseButton) { self.input_state.mouse_button.insert(button); + + UI_CONTEXT.with(|ctx| { + let yak = ctx.borrow(); + let mut yakui = yak.yakui_state.lock(); + let btn = match button { + MouseButton::Left => Some(yakui::input::MouseButton::One), + MouseButton::Right => Some(yakui::input::MouseButton::Two), + MouseButton::Middle => Some(yakui::input::MouseButton::Three), + _ => None, + }; + if let Some(b) = btn { + yakui.handle_event(yakui::event::Event::MouseButtonChanged { + button: b, + down: true, + }); + } + }); } fn mouse_up(&mut self, button: MouseButton) { self.input_state.mouse_button.remove(&button); + + UI_CONTEXT.with(|ctx| { + let yak = ctx.borrow(); + let mut yakui = yak.yakui_state.lock(); + let btn = match button { + MouseButton::Left => Some(yakui::input::MouseButton::One), + MouseButton::Right => Some(yakui::input::MouseButton::Two), + MouseButton::Middle => Some(yakui::input::MouseButton::Three), + _ => None, + }; + if let Some(b) = btn { + yakui.handle_event(yakui::event::Event::MouseButtonChanged { + button: b, + down: false, + }); + } + }); } } @@ -12,7 +12,7 @@ use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry}; use futures::executor; use hecs::{Entity, World}; -use dropbear_engine::future::FutureHandle; +use dropbear_engine::future::{FutureHandle, FutureQueue}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::scene::SceneCommand; use eucalyptus_core::input::InputState; @@ -85,7 +85,6 @@ pub struct PlayMode { main_pipeline: Option<MainRenderPipeline>, shader_globals: Option<GlobalsUniform>, collider_wireframe_pipeline: Option<ColliderWireframePipeline>, - kino_renderer: Option<kino_gui::prelude::KinoRenderer>, initial_scene: Option<String>, current_scene: Option<String>, @@ -109,6 +108,7 @@ pub struct PlayMode { collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>, collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>, + viewport_offset: (f32, f32), } impl PlayMode { @@ -150,10 +150,10 @@ impl PlayMode { collider_wireframe_pipeline: None, collider_wireframe_geometry_cache: HashMap::new(), collider_instance_buffer: None, + viewport_offset: (0.0, 0.0), collision_event_receiver: Some(ce_r), collision_force_event_receiver: Some(cfe_r), event_collector, - kino_renderer: None, }; log::debug!("Created new play mode instance"); @@ -166,20 +166,6 @@ impl PlayMode { self.main_pipeline = Some(MainRenderPipeline::new(graphics.clone())); self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("runtime shader globals"))); self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone())); - - match kino_gui::prelude::KinoRenderer::new( - graphics.device.clone(), - graphics.queue.clone(), - graphics.surface_format, - ) { - Ok(renderer) => { - self.kino_renderer = Some(renderer); - log::debug!("KinoRenderer initialized successfully"); - } - Err(e) => { - log::error!("Failed to initialize KinoRenderer: {}", e); - } - } } fn reload_scripts_for_current_world(&mut self) { @@ -257,7 +243,7 @@ impl PlayMode { let graphics_cloned = graphics.clone(); let component_registry = self.component_registry.clone(); - let handle = graphics.future_queue.push(async move { + let handle = FutureQueue::push(&graphics.future_queue, async move { let mut temp_world = World::new(); let load_status = scene_to_load.load_into_world( &mut temp_world, @@ -278,7 +264,7 @@ impl PlayMode { v } - Err(e) => {panic!("Failed to load scene [{}]: {}", scene_to_load.scene_name, e);} + Err(e) => { panic!("Failed to load scene [{}]: {}", scene_to_load.scene_name, e); } } }); @@ -12,6 +12,10 @@ use hecs::Entity; use wgpu::{Color}; use wgpu::util::DeviceExt; use winit::event_loop::ActiveEventLoop; +use yakui::column; +use yakui::widgets::Button; +use yakui::widgets::{Layer, Pad}; +use yakui_wgpu::SurfaceInfo; use dropbear_engine::camera::Camera; use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; @@ -20,6 +24,7 @@ use dropbear_engine::lighting::{Light, LightComponent}; use dropbear_engine::lighting::MAX_LIGHTS; use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE}; use dropbear_engine::scene::{Scene, SceneCommand}; +use dropbear_engine::texture::Texture; use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::command::CommandBufferPoller; use eucalyptus_core::hierarchy::{EntityTransformExt, Parent}; @@ -31,6 +36,7 @@ use eucalyptus_core::states::SCENES; use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOADER}; use crate::{PlayMode}; use eucalyptus_core::physics::collider::shader::create_wireframe_geometry; +use eucalyptus_core::ui::UI_CONTEXT; impl Scene for PlayMode { fn load(&mut self, graphics: Arc<SharedGraphicsContext>) { @@ -396,7 +402,8 @@ impl Scene for PlayMode { if !p.is_everything_loaded() && p.is_first_scene { // todo: change from label to "splashscreen" ui.centered_and_justified(|ui| { - ui.label("Loading scene..."); + egui_extras::install_image_loaders(&graphics.get_egui_context()); + ui.image(egui::include_image!("../../../resources/eucalyptus-editor.png")) }); return; } @@ -438,6 +445,8 @@ impl Scene for PlayMode { egui::vec2(display_width, display_height), ); + self.viewport_offset = (image_rect.min.x, image_rect.min.y); + ui.allocate_exact_size(available_size, egui::Sense::hover()); ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| { @@ -446,6 +455,34 @@ impl Scene for PlayMode { size: egui::vec2(display_width, display_height), })); }); + + // overlay + UI_CONTEXT.with(|yakui_cell| { + let yak = yakui_cell.borrow(); + let mut yakui = yak.yakui_state.lock(); + + yakui.set_surface_size(yakui::geometry::Vec2::new(display_width, display_height)); + yakui.start(); + + let to_render = yak.to_render.lock().drain(..).collect::<Vec<_>>(); + + Layer::new().show(|| { + column(|| { + for f in to_render { + f() + } + + let button_response = Button::styled("My Button") + .padding(Pad::all(10.0)) + .show(); + if button_response.clicked { + println!("This is clicked!"); + } + }); + }); + + yakui.finish(); + }); } else { log::warn!("No such camera exists in the world"); } @@ -816,46 +853,26 @@ impl Scene for PlayMode { } } } + if let Err(e) = encoder.submit(graphics.clone()) { log_once::error_once!("{}", e); } - } - if let Some(kino_renderer) = &mut self.kino_renderer { - let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino ui render encoder")); - - let commands = eucalyptus_core::ui::UI_COMMAND_BUFFER.drain_commands(); - let ui_buffer = kino_renderer.get_ui(); - - for command in commands { - match command { - eucalyptus_core::ui::UiCommand::Rect { id, initial, size, fill, .. } => { - let rect = kino_gui::prelude::shapes::Rectangle::new( - kino_gui::prelude::WidgetId::new(id), - initial, - size, - fill - ); - ui_buffer.add(rect); + UI_CONTEXT.with(|v| { + let commands = graphics.yakui_renderer.lock().paint( + &mut v.borrow().yakui_state.lock(), + &graphics.device, + &graphics.queue, + SurfaceInfo { + format: Texture::TEXTURE_FORMAT, + sample_count: 1, + color_attachment: &graphics.viewport_texture.view, + resolve_target: None, } - _ => {} - } - } - - let screen_size = kino_gui::prelude::Size { - width: graphics.viewport_texture.size.width as f32, - height: graphics.viewport_texture.size.height as f32, - }; - - kino_renderer.render( - &mut *encoder, - &graphics.viewport_texture.view, - screen_size, - ); + ); - if let Err(e) = encoder.submit(graphics.clone()) { - log_once::error_once!("Failed to submit kino renderer: {}", e); - } + graphics.queue.submit([commands]); + }); } } @@ -0,0 +1,75 @@ +package com.dropbear.sample + +DynamicBoundingBox { + Circle { + name = "status_backing" + + align { + horizontal = Alignment.Left + 10 + vertical = Alignment.Centered + } + + radius = 40.0 + + Image { + name = "status_image" + + when (Player.playerState) { + PlayerState.Gas -> { + resource = "euca://player/status/gas.png" + } + PlayerState.Solid -> { + resource = "euca://player/status/solid.png" + } + PlayerState.Liquid -> { + resource = "euca://player/status/liquid.png" + } + } + + align = Align.default() + } + } + + bar("health_bar", Align.Center + 20.0) + + bar("energy_bar", Align.Center - 20.0) +} + +bar(val objectName: String, align: Align = Align.default()): DynamicBoundingBox { + Rectangle { + name = objectName + " backing" + + size = Vector2d(30.0, 10.0) + + align = align + + style { + fill = null + stroke = Stroke( + colour = Colour.BLACK, + width = 1.0 + ) + } + } + + Rectangle { + name = objectName + " fill" + + size = Vector2d(Player.health.percentage() * 30.0, 10.0) + + align = align + + style { + fill = Fill( + colour = when (Player.currentZone) { + CurrentZone.Freezing -> Colour.DARK_BLUE + CurrentZone.Cold -> Colour.LIGHT_BLUE + CurrentZone.Normal -> Colour.LIGHT_GREY + CurrentZone.Hot -> Colour.ORANGE + CurrentZone.Boiling -> Colour.SCARLET_RED + } + ) + stroke = null + } + } +} @@ -0,0 +1,6 @@ +package com.dropbear.ui + +class Align { + var horizontal: Alignment = Alignment.Center + var vertical: Alignment = Alignment.Center +} @@ -0,0 +1,10 @@ +package com.dropbear.ui + +enum class Alignment { + Left, + Center, + Right, + + Top, + Bottom +} @@ -1,62 +0,0 @@ -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") -} @@ -1,69 +0,0 @@ -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) - } - } -} @@ -1,21 +0,0 @@ -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,33 @@ +package com.dropbear.ui + +import com.dropbear.ui.widgets.* + +interface UIContainer { + fun addChild(child: UIElement) + fun getChildren(): List<UIElement> + + fun Circle(block: Circle.() -> Unit) { + val circle = Circle().apply(block) + addChild(circle) + } + + fun Rectangle(block: Rectangle.() -> Unit) { + val rectangle = Rectangle().apply(block) + addChild(rectangle) + } + + fun Image(block: Image.() -> Unit) { + val image = Image().apply(block) + addChild(image) + } + + fun Text(block: Text.() -> Unit) { + val text = Text().apply(block) + addChild(text) + } + + fun DynamicBoundingBox(block: DynamicBoundingBox.() -> Unit) { + val box = DynamicBoundingBox().apply(block) + addChild(box) + } +} @@ -0,0 +1,12 @@ +package com.dropbear.ui + +import com.dropbear.DropbearEngine + +open class UIElement { + var name: String = "noNamedUIElement" + internal var parent: UIElement? = null + + var onClick: ((DropbearEngine, UIElement) -> Unit)? = null + var onHover: ((DropbearEngine, UIElement) -> Unit)? = null + var onHoverExit: ((DropbearEngine, UIElement) -> Unit)? = null +} @@ -11,18 +11,5 @@ import com.dropbear.utils.ID * 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 +} @@ -1,13 +0,0 @@ -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 -} @@ -1,6 +0,0 @@ -package com.dropbear.ui - -object WidgetType { - const val RECTANGLE = 0 - const val CIRCLE = 1 -} @@ -1,25 +0,0 @@ -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) - } -} @@ -1,37 +0,0 @@ -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) - } -} @@ -0,0 +1,5 @@ +package com.dropbear.ui.styling + +import com.dropbear.utils.Colour + +class Fill(colour: Colour = Colour.TRANSPARENT) @@ -0,0 +1,8 @@ +package com.dropbear.ui.styling + +import com.dropbear.utils.Colour + +class Stroke( + colour: Colour = Colour.BLACK, + width: Double = 1.0, +) @@ -0,0 +1,6 @@ +package com.dropbear.ui.styling + +class StyleConfig { + var fill: Fill? = null + var stroke: Stroke? = null +} @@ -0,0 +1,22 @@ +package com.dropbear.ui.widgets + +import com.dropbear.ui.Align +import com.dropbear.ui.UIContainer +import com.dropbear.ui.UIElement + +class Circle : UIElement(), UIContainer { + var radius: Double = 0.0 + var alignment: Align? = null + private val children = mutableListOf<UIElement>() + + fun align(block: Align.() -> Unit) { + alignment = Align().apply(block) + } + + override fun addChild(child: UIElement) { + child.parent = this + children.add(child) + } + + override fun getChildren(): List<UIElement> = children.toList() +} @@ -0,0 +1,29 @@ +package com.dropbear.ui.widgets + +import com.dropbear.ui.UIContainer +import com.dropbear.ui.UIElement + +class DynamicBoundingBox: UIElement(), UIContainer { + private val children = mutableListOf<UIElement>() + + override fun addChild(child: UIElement) { + child.parent = this + children.add(child) + } + + override fun getChildren(): List<UIElement> = children + + fun findByName(name: String): UIElement? { + if (this.name == name) return this + for (child in children) { + if (child.name == name) return child + if (child is UIContainer) { + if (child is DynamicBoundingBox) { + val found = child.findByName(name) + if (found != null) return found + } + } + } + return null + } +} @@ -0,0 +1,9 @@ +package com.dropbear.ui.widgets + +import com.dropbear.ui.Align +import com.dropbear.ui.UIElement + +class Image : UIElement() { + var resource: String = "" + var align: Align? = null +} @@ -0,0 +1,26 @@ +package com.dropbear.ui.widgets + +import com.dropbear.math.Vector2d +import com.dropbear.ui.Align +import com.dropbear.ui.UIContainer +import com.dropbear.ui.UIElement +import com.dropbear.ui.styling.StyleConfig + +class Rectangle : UIElement(), UIContainer { + var size: Vector2d = Vector2d(0.0, 0.0) + var align: Align? = null + var styleConfig: StyleConfig? = null + + private val children = mutableListOf<UIElement>() + + fun style(block: StyleConfig.() -> Unit) { + styleConfig = StyleConfig().apply(block) + } + + override fun addChild(child: UIElement) { + child.parent = this + children.add(child) + } + + override fun getChildren(): List<UIElement> = children.toList() +} @@ -0,0 +1,12 @@ +package com.dropbear.ui.widgets + +import com.dropbear.ui.Align +import com.dropbear.ui.UIElement +import com.dropbear.utils.Colour + +class Text : UIElement() { + var content: String = "empty..." + var align: Align? = null + var fontSize: Double = 12.0 + var color: Colour = Colour.BLACK +} @@ -18,6 +18,7 @@ class Colour( var a: UByte, ) { companion object { + val TRANSPARENT = Colour(0u, 0u, 0u, 255u) val WHITE = Colour(0u, 0u, 0u, 255u) val BLACK = Colour(255u, 255u, 255u, 255u) @@ -1,15 +0,0 @@ -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); -} @@ -1,24 +1 @@ -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 - ) -} +package com.dropbear.ui @@ -1,18 +1 @@ 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") -}