tirbofish/dropbear · commit
c4d1e1674ac314c5a649379533fe70ffd4f08583
feature: implemented text (but still on wgpu 27.0) :(
feature: implemented basic input checking
docs: new readme.md
feature: got texturing working
Signature present but could not be verified.
Unverified
@@ -80,6 +80,7 @@ yakui-winit = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f thiserror = "2.0" tempfile = "3.24" combine = "4.6" +glyphon = "0.8" [workspace.dependencies.image] version = "0.25" @@ -3,8 +3,6 @@ <h1 align="center">dropbear</h1> </p> - dropbear is a game engine used to create games, made in Rust and scripted with the Kotlin Language. It's name is a double entendre, with it being the nickname of koalas but also fits in nicely with the theme of rust utilising memory management with "drops". @@ -18,7 +16,7 @@ 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. -- [slank](https://github.com/tirbofish/dropbear/tree/main/crates/slank) is a slang compiler that compiles .slang files into shaders during build.rs compilation time. +- [kino_ui](https://github.com/tirbofish/dropbear/tree/main/crates/kino_ui) is the main runtime-side UI system to render widgets and elements. [//]: # (- [eucalyptus-sdk](https://github.com/tirbofish/dropbear/tree/main/eucalyptus-sdk) is used to develop plugins to be used with the `eucalyptus-editor`) @@ -26,6 +24,7 @@ If you might have not realised, all the crates/projects names are after Australi - [dropbear_future-queue](https://github.com/tirbofish/dropbear/tree/main/dropbear_future-queue) is a handy library for dealing with async in a sync context - [model_to_image](https://github.com/tirbofish/model_to_image) is a library used to generate thumbnails and images from a 3D model with the help of `russimp-ng` and a custom made rasterizer. _(very crude but usable)_ +- [slank](https://github.com/tirbofish/dropbear/tree/main/crates/slank) is a slang compiler that compiles .slang files into shaders during build.rs compilation time. ## Build @@ -23,14 +23,12 @@ 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, previous_world: None } + Self { local, world } } /// Creates a new [EntityTransform] from a world [Transform] and a default local transform. @@ -40,7 +38,6 @@ impl EntityTransform { Self { world, local: Transform::default(), - previous_world: None, } } @@ -77,26 +74,6 @@ 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 @@ -227,11 +227,11 @@ impl CommandEncoder { /// Submits the command encoder for execution. /// /// Panics if an unwinding error is caught, or just returns the error as normal. - pub fn submit(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> { + pub fn submit(self) -> anyhow::Result<()> { let command_buffer = self.inner.finish(); match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - graphics.queue.submit(std::iter::once(command_buffer)); + self.queue.submit(std::iter::once(command_buffer)); })) { Ok(_) => {Ok(())} Err(_) => { @@ -480,7 +480,7 @@ Hardware: }); } - if let Err(e) = encoder.submit(graphics.clone()) { + if let Err(e) = encoder.submit() { log_once::error_once!("{}", e); } } @@ -1,4 +1,4 @@ -#![windows_subsystem = "windows"] +#![cfg_attr(not(debug), windows_subsystem = "windows")] use app_dirs2::AppInfo; use dropbear_engine::future::FutureQueue; @@ -413,7 +413,7 @@ impl Scene for Editor { }); } - if let Err(e) = encoder.submit(graphics.clone()) { + if let Err(e) = encoder.submit() { log_once::error_once!("{}", e); } } @@ -717,7 +717,7 @@ impl Scene for Editor { } } } - if let Err(e) = encoder.submit(graphics.clone()) { + if let Err(e) = encoder.submit() { log_once::error_once!("{}", e); } } @@ -101,7 +101,7 @@ async fn main() -> anyhow::Result<()> { ) .filter(Some("eucalyptus_core"), LevelFilter::Debug) .filter(Some("dropbear_traits"), LevelFilter::Debug) - .filter(Some("kino_ui"), LevelFilter::Trace) + .filter(Some("kino_ui"), LevelFilter::Debug) .init(); log::info!("Initialised logger"); } @@ -15,3 +15,6 @@ bytemuck.workspace = true image.workspace = true anyhow.workspace = true log.workspace = true +winit.workspace = true + +glyphon.workspace = true @@ -0,0 +1,45 @@ +# kino-ui + +Kino is a type of resin that is made by eucalyptus trees, and the name of the UI library. + +Built with wgpu and winit, this UI library is inspired by the ui crate [wick3dr0se/egor](https://github.com/wick3dr0se/egor) and uses +Assembly-like instructions to render different components, including standard and contained widgets. + +# Example + +```rust +pub fn init() { + let renderer = KinoWGPURenderer::new(/*yadda yadda doo*/); + let windowing = KinoWinitWindowing::new(window.clone()); + + // keep this with you this is important. the rest are owned by KinoState + let kino = KinoState::new(renderer, windowing); +} + +pub fn update(kino: &mut KinoState) { + // creating new widget + let new_rect = kino.add_widget(Box::new( + Rectangle::new("red patch".into()) + .with(&Rect::new([50.0, 100.0].into(), [128.0, 100.0].into())) + .colour([255.0, 0.0, 0.0, 255.0]).into() + )); + + // polling to build the widget tree + kino.poll(); + + // checking input response + if kino.response(new_rect).clicked { + println!("I have been clicked!"); + } +} + +pub fn render(kino: &mut KinoState) { + // ... + + // generally the last thing to render on your viewport if you want + // an overlay + kino.render(/**/); + + // ... +} +``` @@ -1,14 +1,24 @@ use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::collections::hash_map::DefaultHasher; use std::marker::PhantomData; use crate::rendering::texture::Texture; /// A handle with type [`T`] that provides an index to the [AssetServer] contents. -#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] +#[derive(Hash, Eq, PartialEq, Debug)] pub struct Handle<T> { pub id: u64, _phanton: PhantomData<T> } +impl<T> Copy for Handle<T> {} + +impl<T> Clone for Handle<T> { + fn clone(&self) -> Self { + *self + } +} + impl<T> Handle<T> { pub fn new(id: u64) -> Self { Self { id, _phanton: Default::default() } @@ -19,6 +29,7 @@ impl<T> Handle<T> { #[derive(Default)] pub struct AssetServer { textures: HashMap<u64, Texture>, + texture_labels: HashMap<String, Handle<Texture>>, } impl AssetServer { @@ -26,6 +37,7 @@ impl AssetServer { pub fn new() -> AssetServer { AssetServer { textures: Default::default(), + texture_labels: Default::default(), } } @@ -35,17 +47,50 @@ impl AssetServer { /// you can use [`Texture::from_bytes`]. pub fn add_texture(&mut self, texture: Texture) -> Handle<Texture> { let handle = Handle::new(texture.hash); - self.textures.insert(handle.id, texture); + self.textures.entry(handle.id).or_insert(texture); handle } + /// Adds a texture with a label. If the texture already exists (by hash), + /// returns the existing handle and updates the label to point at it. + pub fn add_texture_with_label(&mut self, label: impl Into<String>, texture: Texture) -> Handle<Texture> { + let handle = self.add_texture(texture); + self.texture_labels.insert(label.into(), handle.clone()); + handle + } + + /// Maps a label to an existing texture handle. + pub fn label_texture(&mut self, label: impl Into<String>, handle: Handle<Texture>) { + self.texture_labels.insert(label.into(), handle.clone()); + } + /// Updates the asset server by inserting the texture provided at the location of the handle, /// and removing the old texture (by returning it back to you). pub fn update_texture(&mut self, handle: Handle<Texture>, texture: Texture) -> Option<Texture> { self.textures.insert(handle.id, texture) } - pub fn get_texture(&mut self, handle: Handle<Texture>) -> Option<&Texture> { + pub fn get_texture(&self, handle: Handle<Texture>) -> Option<&Texture> { self.textures.get(&handle.id) } + + pub fn get_texture_by_label(&self, label: &str) -> Option<&Texture> { + self.texture_labels + .get(label) + .and_then(|handle| self.textures.get(&handle.id)) + } + + pub fn get_texture_handle(&self, label: &str) -> Option<Handle<Texture>> { + self.texture_labels.get(label).cloned() + } + + pub fn texture_handle_by_hash(&self, hash: u64) -> Option<Handle<Texture>> { + self.textures.contains_key(&hash).then(|| Handle::new(hash)) + } + + pub fn hash_bytes(data: &[u8]) -> u64 { + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + hasher.finish() + } } @@ -18,7 +18,7 @@ impl Default for Camera2D { impl Camera2D { /// Returns the orthographic view-projection matrix for the current camera state - pub(crate) fn view_proj(&self, screen_size: Vec2) -> Mat4 { + pub fn view_proj(&self, screen_size: Vec2) -> Mat4 { let width = screen_size.x / self.zoom; let height = screen_size.y / self.zoom; @@ -1,6 +1,4 @@ -//! kino-ui, a UI library with instruction set UI rendering. -//! -//! Uses wgpu for rendering and winit for input management. +#![doc = include_str!("../README.md")] pub mod resp; pub mod widgets; @@ -8,23 +6,29 @@ pub mod rendering; pub mod camera; pub mod asset; pub mod math; +pub mod windowing; use crate::asset::{AssetServer, Handle}; use crate::camera::Camera2D; use crate::rendering::texture::Texture; use crate::rendering::vertex::Vertex; -use crate::rendering::{KinoWGPURenderer, VertexBatch}; +use crate::rendering::{KinoWGPURenderer}; use crate::resp::WidgetResponse; use crate::widgets::{ContaineredWidget, NativeWidget}; +use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; use std::fmt::Debug; use std::hash::{DefaultHasher, Hash, Hasher}; use wgpu::{LoadOp, StoreOp}; +use rendering::batching::VertexBatch; +use crate::windowing::KinoWinitWindowing; +use glam::Vec2; /// Holds the state of all the instructions, and the vertices+indices for rendering as well /// as the responses. pub struct KinoState { renderer: KinoWGPURenderer, + windowing: KinoWinitWindowing, instruction_set: VecDeque<UiInstructionType>, widget_states: HashMap<WidgetId, WidgetResponse>, assets: AssetServer, @@ -36,10 +40,11 @@ impl KinoState { /// Creates a new instance of a [KinoState]. /// /// This sits inside your `init()` function. - pub fn new(renderer: KinoWGPURenderer) -> KinoState { + pub fn new(renderer: KinoWGPURenderer, windowing: KinoWinitWindowing) -> Self { log::debug!("Created KinoState"); KinoState { renderer, + windowing, instruction_set: Default::default(), widget_states: Default::default(), assets: Default::default(), @@ -48,11 +53,19 @@ impl KinoState { } } - pub fn add_widget(&mut self, widget: Box<dyn NativeWidget>) { + /// Adds a widget (a [`NativeWidget`]) to the instruction set as a + /// [`UiInstructionType::Widget`] and returns back the associated [`WidgetId`] for response + /// checking. + pub fn add_widget(&mut self, widget: Box<dyn NativeWidget>) -> WidgetId { + let id = widget.id(); self.instruction_set.push_back(UiInstructionType::Widget(widget)); + id } - pub fn add_container(&mut self, _container: Box<dyn ContaineredWidget>) { + /// Adds a widget (a [`ContaineredWidget`]) to the instruction set as a + /// [`UiInstructionType::Containered`] and returns the associated [`WidgetId`] for response + /// checking. + pub fn add_container(&mut self, _container: Box<dyn ContaineredWidget>) -> WidgetId { todo!("This is broken rn and idk how to implement it") // self.instruction_set.push_back(UiInstructionType::Containered( // ContaineredWidgetType::Start { @@ -62,11 +75,17 @@ impl KinoState { // )) } + /// Adds a [UiInstructionType] to the instruction set. pub fn add_instruction(&mut self, ui_instruction_type: UiInstructionType) { self.instruction_set.push_back(ui_instruction_type); } - /// Polls for changes, builds the tree and prepares them for rendering. + /// Polls for changes by clearing the current instruction set, build the tree and + /// preparing them for rendering. + /// + /// If you create a widget and then check for a response before polling, you will not receive + /// back a response. You are required to poll/prepare the contents before being given access + /// to the response information. /// /// This sits inside your `update()` loop. pub fn poll(&mut self) { @@ -82,8 +101,23 @@ impl KinoState { self.render_tree(tree); } + /// Fetches the [`WidgetResponse`] from an associated [`WidgetId`]. + /// + /// This will only provide the proper information **after** you have + /// polled with [`KinoState::poll`]. + pub fn response(&self, id: WidgetId) -> WidgetResponse { + self.widget_states.get(&id).copied().unwrap_or_default() + } + + pub fn set_viewport_offset(&mut self, offset: Vec2) { + self.windowing.viewport_offset = offset; + } + /// Pushes the vertices and indices to the renderer. /// + /// This is the recommended `render()` function and is used when you want + /// `kino_ui` to create the render pass and submit to the queue. + /// /// This sits inside your `render()` loop. pub fn render( &mut self, @@ -119,13 +153,130 @@ impl KinoState { }); for mut tg in batch { - log::debug!("Rendering textured geometry: {:?}", tg); + // log::debug!("Rendering textured geometry: {:?}", tg); let texture = tg.texture_id.and_then(|v| { self.assets.get_texture(v) }); self.renderer.draw_batch(&mut pass, device, queue, &mut tg.batch, texture); } + + // self.renderer.text.render(&mut pass); + } + } + + /// Pushes the vertices and indices to the renderer. + /// + /// This is used when you want control on the [`wgpu::RenderPass`] and you want + /// `kino_ui` to only draw the widgets. + /// + /// This sits inside your `render()` loop. + pub fn render_into_pass( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + pass: &mut wgpu::RenderPass<'_>, + ) { + self.renderer.upload_camera_matrix( + queue, + self.camera + .view_proj(self.renderer.size) + .to_cols_array_2d(), + ); + let batch = self.batch.take(); + + for mut tg in batch { + // log::debug!("Rendering textured geometry: {:?}", tg); + let texture = tg.texture_id.and_then(|v| { + self.assets.get_texture(v) + }); + self.renderer.draw_batch(pass, device, queue, &mut tg.batch, texture); + } + + // self.renderer.text.render(&mut pass); + } + + /// Handles the event into the internal input state. + /// + /// This is not required, however if you want reactivity, include it into your WindowEvent code. + pub fn handle_event(&mut self, event: &winit::event::WindowEvent) { + self.windowing.handle_event(event); + } + + /// Returns a mutable reference to the internal [`AssetServer`], used + /// for storing textures. + pub fn assets(&mut self) -> &mut AssetServer { + &mut self.assets + } + + /// Creates (or reuses) a texture from raw RGBA bytes or encoded image bytes + /// (e.g. `include_bytes!()` PNG/JPEG) and stores it by label. + /// If a texture with the same content hash already exists, it reuses the handle + /// and just updates the label mapping. + pub fn add_texture_from_bytes( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + label: impl Into<String>, + data: &[u8], + width: u32, + height: u32, + ) -> Handle<Texture> { + let mut raw: Cow<[u8]> = Cow::Borrowed(data); + let mut width = width; + let mut height = height; + let expected_len = (width as usize) + .saturating_mul(height as usize) + .saturating_mul(4); + + if data.len() != expected_len { + match image::load_from_memory(data) { + Ok(img) => { + let rgba = img.to_rgba8(); + let (w, h) = rgba.dimensions(); + width = w; + height = h; + raw = Cow::Owned(rgba.into_raw()); + } + Err(e) => { + log::error!("Failed to decode texture bytes: {}", e); + let fallback = Texture::create_default( + device, + queue, + self.renderer.texture_bind_group_layout(), + ); + return self.assets.add_texture_with_label(label, fallback); + } + } } + + let hash = AssetServer::hash_bytes(raw.as_ref()); + if let Some(handle) = self.assets.texture_handle_by_hash(hash) { + self.assets.label_texture(label, handle.clone()); + return handle; + } + + let texture = Texture::from_bytes( + device, + queue, + self.renderer.texture_bind_group_layout(), + raw.as_ref(), + width, + height, + ); + self.assets.add_texture_with_label(label, texture) + } + + /// Fetch a texture handle by label. + pub fn texture_handle(&self, label: &str) -> Option<Handle<Texture>> { + self.assets.get_texture_handle(label) + } + + pub(crate) fn input(&self) -> &KinoWinitWindowing { + &self.windowing + } + + pub(crate) fn set_response(&mut self, id: WidgetId, response: WidgetResponse) { + self.widget_states.insert(id, response); } fn build_tree(instructions: Vec<UiInstructionType>) -> Vec<UiNode> { @@ -195,9 +346,16 @@ impl KinoState { } } +/// The id of the widget, often being a hash. #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] pub struct WidgetId(pub u64); +impl WidgetId { + pub fn from_str(str: &str) -> Self { + str.into() + } +} + impl Into<WidgetId> for &str { fn into(self) -> WidgetId { let mut hasher = DefaultHasher::new(); @@ -1,5 +1,6 @@ use glam::{vec2, Vec2}; +/// A rectangular shape consisting of a `position` and a `size`. #[derive(Copy, Clone, PartialEq, Debug)] pub struct Rect { pub position: Vec2, @@ -27,7 +28,7 @@ impl Rect { self.position + self.size * 0.5 } - // Move the rectangle by the given delta vector + /// Move the rectangle by the given delta vector pub fn translate(&mut self, delta: Vec2) { self.position += delta; } @@ -1,18 +1,21 @@ use glam::Vec2; -use wgpu::{BufferUsages, IndexFormat}; use wgpu::util::{BufferInitDescriptor, DeviceExt}; +use batching::VertexBatch; use crate::camera::{CameraRendering, CameraUniform}; use crate::rendering::pipeline::KinoRendererPipeline; -use crate::rendering::vertex::Vertex; +// use crate::rendering::text::KinoTextRenderer; pub mod pipeline; pub mod texture; pub mod vertex; +pub mod batching; +// pub mod text; pub struct KinoWGPURenderer { pipeline: KinoRendererPipeline, default_texture: texture::Texture, pub size: Vec2, + // pub text: KinoTextRenderer, camera: CameraRendering, } @@ -51,11 +54,14 @@ impl KinoWGPURenderer { }); let default_texture = texture::Texture::create_default(&device, &queue, &pipeline.texture_bind_group_layout); + // let text = KinoTextRenderer::new(&device, &queue, surface_format); + log::debug!("Created KinoWGPURenderer"); Self { pipeline, default_texture, size: Vec2::from_array(size), + // text, camera: CameraRendering { buffer: camera_buffer, bind_group: camera_bind_group, @@ -94,122 +100,9 @@ impl KinoWGPURenderer { batch.draw(r_pass); batch.clear(); } -} -/// Describes a primitive shape. -#[derive(Debug)] -pub struct VertexBatch { - vertices: Vec<Vertex>, - indices: Vec<u16>, - vertex_buffer: Option<wgpu::Buffer>, - index_buffer: Option<wgpu::Buffer>, - vertices_dirty: bool, - indices_dirty: bool, -} - -impl Default for VertexBatch { - fn default() -> Self { - Self { - vertices: Vec::with_capacity(Self::MAX_VERTICES), - indices: Vec::with_capacity(Self::MAX_INDICES), - vertex_buffer: None, - index_buffer: None, - vertices_dirty: false, - indices_dirty: false, - } + pub(crate) fn texture_bind_group_layout(&self) -> &wgpu::BindGroupLayout { + &self.pipeline.texture_bind_group_layout } } -impl VertexBatch { - const MAX_VERTICES: usize = u16::MAX as usize; - const MAX_INDICES: usize = Self::MAX_VERTICES * 6; - - /// Returns true if adding verts/indices would exceed max allowed - fn would_overflow(&self, vert_count: usize, idx_count: usize) -> bool { - self.vertices.len() + vert_count > Self::MAX_VERTICES - || self.indices.len() + idx_count > Self::MAX_INDICES - } - - /// Adds vertices/indices, returns false if it would overflow - pub fn push(&mut self, verts: &[Vertex], indices: &[u16]) -> bool { - if self.would_overflow(verts.len(), indices.len()) { - return false; - } - - let idx_offset = self.vertices.len() as u16; - self.vertices.extend_from_slice(verts); - self.indices.extend(indices.iter().map(|i| *i + idx_offset)); - - self.vertices_dirty = true; - self.indices_dirty = true; - - true - } - - fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { - if self.is_empty() || (!self.vertices_dirty && !self.indices_dirty) { - return; - } - - if self.vertex_buffer.is_none() { - self.vertex_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { - label: Some("kino vertex buffer"), - size: (Self::MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64, - usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, - mapped_at_creation: false, - })); - } - if self.index_buffer.is_none() { - self.index_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { - label: Some("kino index buffer"), - size: (Self::MAX_INDICES * std::mem::size_of::<u16>()) as u64, - usage: BufferUsages::INDEX | BufferUsages::COPY_DST, - mapped_at_creation: false, - })); - } - - if self.vertices_dirty { - queue.write_buffer( - self.vertex_buffer.as_ref().unwrap(), - 0, - bytemuck::cast_slice(&self.vertices), - ); - self.vertices_dirty = false; - } - if self.indices_dirty { - let mut indices_bytes: Vec<u8> = bytemuck::cast_slice(&self.indices).to_vec(); - let remainder = indices_bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize; - if remainder != 0 { - let pad_len = wgpu::COPY_BUFFER_ALIGNMENT as usize - remainder; - indices_bytes.extend_from_slice(&vec![0u8; pad_len]); - } - - queue.write_buffer(self.index_buffer.as_ref().unwrap(), 0, &indices_bytes); - self.indices_dirty = false; - } - } - - fn is_empty(&self) -> bool { - self.vertices.is_empty() || self.indices.is_empty() - } - - fn clear(&mut self) { - self.vertices.clear(); - self.indices.clear(); - self.vertices_dirty = true; - self.indices_dirty = true; - } - - fn draw(&self, pass: &mut wgpu::RenderPass) { - if self.is_empty() { - return; - } - - pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..)); - pass.set_index_buffer( - self.index_buffer.as_ref().unwrap().slice(..), - IndexFormat::Uint16, - ); - pass.draw_indexed(0..self.indices.len() as u32, 0, 0..1); - } -} @@ -0,0 +1,120 @@ +use wgpu::{BufferUsages, IndexFormat}; +use crate::rendering::vertex::Vertex; + +/// Describes a primitive shape. +#[derive(Debug)] +pub struct VertexBatch { + vertices: Vec<Vertex>, + indices: Vec<u16>, + vertex_buffer: Option<wgpu::Buffer>, + index_buffer: Option<wgpu::Buffer>, + vertices_dirty: bool, + indices_dirty: bool, +} + +impl Default for VertexBatch { + fn default() -> Self { + Self { + vertices: Vec::with_capacity(Self::MAX_VERTICES), + indices: Vec::with_capacity(Self::MAX_INDICES), + vertex_buffer: None, + index_buffer: None, + vertices_dirty: false, + indices_dirty: false, + } + } +} + +impl VertexBatch { + const MAX_VERTICES: usize = u16::MAX as usize; + const MAX_INDICES: usize = Self::MAX_VERTICES * 6; + + /// Returns true if adding verts/indices would exceed max allowed + fn would_overflow(&self, vert_count: usize, idx_count: usize) -> bool { + self.vertices.len() + vert_count > Self::MAX_VERTICES + || self.indices.len() + idx_count > Self::MAX_INDICES + } + + /// Adds vertices/indices, returns false if it would overflow + pub fn push(&mut self, verts: &[Vertex], indices: &[u16]) -> bool { + if self.would_overflow(verts.len(), indices.len()) { + return false; + } + + let idx_offset = self.vertices.len() as u16; + self.vertices.extend_from_slice(verts); + self.indices.extend(indices.iter().map(|i| *i + idx_offset)); + + self.vertices_dirty = true; + self.indices_dirty = true; + + true + } + + pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { + if self.is_empty() || (!self.vertices_dirty && !self.indices_dirty) { + return; + } + + if self.vertex_buffer.is_none() { + self.vertex_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { + label: Some("kino vertex buffer"), + size: (Self::MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64, + usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, + mapped_at_creation: false, + })); + } + if self.index_buffer.is_none() { + self.index_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { + label: Some("kino index buffer"), + size: (Self::MAX_INDICES * std::mem::size_of::<u16>()) as u64, + usage: BufferUsages::INDEX | BufferUsages::COPY_DST, + mapped_at_creation: false, + })); + } + + if self.vertices_dirty { + queue.write_buffer( + self.vertex_buffer.as_ref().unwrap(), + 0, + bytemuck::cast_slice(&self.vertices), + ); + self.vertices_dirty = false; + } + if self.indices_dirty { + let mut indices_bytes: Vec<u8> = bytemuck::cast_slice(&self.indices).to_vec(); + let remainder = indices_bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize; + if remainder != 0 { + let pad_len = wgpu::COPY_BUFFER_ALIGNMENT as usize - remainder; + indices_bytes.extend_from_slice(&vec![0u8; pad_len]); + } + + queue.write_buffer(self.index_buffer.as_ref().unwrap(), 0, &indices_bytes); + self.indices_dirty = false; + } + } + + pub fn is_empty(&self) -> bool { + self.vertices.is_empty() || self.indices.is_empty() + } + + pub fn clear(&mut self) { + self.vertices.clear(); + self.indices.clear(); + self.vertices_dirty = true; + self.indices_dirty = true; + } + + pub fn draw(&self, pass: &mut wgpu::RenderPass) { + if self.is_empty() { + return; + } + + pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..)); + pass.set_index_buffer( + self.index_buffer.as_ref().unwrap().slice(..), + IndexFormat::Uint16, + ); + pass.draw_indexed(0..self.indices.len() as u32, 0, 0..1); + } +} @@ -0,0 +1,97 @@ +use glam::Vec2; +use glyphon::{Buffer, Cache, Color, FontSystem, Resolution, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport}; + +pub struct TextEntry { + pub buffer: Buffer, + pub position: Vec2, +} + +pub struct KinoTextRenderer { + pub font_system: + FontSystem, + pub swash_cache: SwashCache, + pub atlas: TextAtlas, + pub renderer: TextRenderer, + pub viewport: Viewport, + pub entries: Vec<TextEntry>, +} + +impl KinoTextRenderer { + pub(crate) fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self { + log::debug!("Creating KinoTextRenderer"); + log::debug!("Loading system fonts"); + let mut font_system = FontSystem::new(); + font_system + .db_mut() + .load_system_fonts(); + log::debug!("Loaded system fonts"); + + log::debug!("Loading \"Roboto-Regular.ttf\" as fallback"); + font_system.db_mut().load_font_data(include_bytes!("../../../../resources/fonts/Roboto-Regular.ttf").to_vec()); + log::debug!("Loaded fallback"); + let swash_cache = SwashCache::new(); + let cache = Cache::new(device); + let viewport = Viewport::new(device, &cache); + let mut atlas = TextAtlas::new(device, queue, &cache, format); + let renderer = TextRenderer::new(&mut atlas, device, Default::default(), None); + + log::debug!("Created new KinoTextRenderer"); + + Self { + font_system, + swash_cache, + atlas, + renderer, + viewport, + entries: Vec::new(), + } + } + + /// Prepare the text renderer for drawing + pub(crate) fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) { + if self.entries.is_empty() { + return; + } + + let text_areas: Vec<TextArea> = self + .entries + .iter() + .map(|entry| TextArea { + buffer: &entry.buffer, + left: entry.position.x, + top: entry.position.y, + bounds: TextBounds { + right: width as i32, + bottom: height as i32, + ..Default::default() + }, + scale: 1.0, + default_color: Color::rgb(255, 255, 255), + custom_glyphs: &[], + }) + .collect(); + self.renderer + .prepare( + device, + queue, + &mut self.font_system, + &mut self.atlas, + &self.viewport, + text_areas, + &mut self.swash_cache, + ) + .unwrap(); + + self.entries.clear(); + } + + pub(crate) fn render<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) { + self.renderer + .render(&self.atlas, &self.viewport, pass) + .unwrap(); + } + + pub(crate) fn resize(&mut self, width: u32, height: u32, queue: &wgpu::Queue) { + self.viewport.update(queue, Resolution { width, height }); + } +} @@ -2,5 +2,4 @@ pub struct WidgetResponse { pub clicked: bool, pub hovering: bool, - pub checked: bool, } @@ -1,4 +1,5 @@ pub mod rect; +// pub mod text; use std::any::Any; use crate::{KinoState, UiNode, WidgetId}; @@ -6,6 +6,8 @@ use crate::math::Rect; use crate::rendering::texture::Texture; use crate::rendering::vertex::Vertex; use crate::widgets::{Anchor, NativeWidget}; +use crate::resp::WidgetResponse; +use winit::event::{ElementState, MouseButton}; pub struct Rectangle { /// The identifier of the widget. @@ -44,7 +46,7 @@ impl Rectangle { position: Vec2::ZERO, size: vec2(64.0, 64.0), rotation: 0.0, - colour: [255.0, 255.0, 255.0, 255.0], + colour: [1.0, 1.0, 1.0, 1.0], uvs: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], texture: None, } @@ -72,8 +74,8 @@ impl Rectangle { self.size = size; self } - /// Sets the color of the rectangle - pub fn color(mut self, colour: [f32; 4]) -> Self { + /// Sets the colour of the rectangle + pub fn colour(mut self, colour: [f32; 4]) -> Self { self.colour = colour; self } @@ -104,6 +106,18 @@ impl NativeWidget for Rectangle { }; let top_left = self.position + offset; let rect = Rect::new(top_left, self.size); + let input = state.input(); + let hovering = rect.contains(input.mouse_position); + let clicked = hovering + && input.mouse_button == MouseButton::Left + && input.mouse_press_state == ElementState::Pressed; + state.set_response( + self.id, + WidgetResponse { + clicked, + hovering, + }, + ); let rot = Mat2::from_angle(self.rotation); let verts: Vec<_> = rect .corners() @@ -0,0 +1,74 @@ +use std::any::Any; +use glam::Vec2; +use glyphon::{Attrs, AttrsOwned, Buffer, Color, Metrics, Shaping}; +use crate::{KinoState, WidgetId}; +use crate::rendering::text::TextEntry; +use crate::widgets::NativeWidget; + +pub struct Text { + pub id: WidgetId, + pub text: String, + pub position: Vec2, + pub metrics: Metrics, + pub attributes: AttrsOwned, +} + +impl Text { + /// Create a new text builder that will push text to the renderer + pub fn new(text: String) -> Self { + Self { + id: WidgetId::from_str(&text), + text, + position: Vec2::new(10.0, 10.0), + metrics: Metrics::new(16.0, 1.0), + attributes: AttrsOwned::new(Attrs::new()), + } + } + + /// Set a custom ID before sending the widget off + pub fn with_id(mut self, id: WidgetId) -> Self { + self.id = id; + self + } + + /// Set the position of text in screen space + pub fn at(mut self, position: impl Into<Vec2>) -> Self { + self.position = position.into(); + self + } + + pub fn with_attrs(mut self, attributes: AttrsOwned) -> Self { + self.attributes = attributes; + self + } + + pub fn with_metrics(mut self, metrics: Metrics) -> Self { + self.metrics = metrics; + self + } +} + +impl NativeWidget for Text { + fn render(self: Box<Self>, state: &mut KinoState) { + let mut buffer = Buffer::new(&mut state.renderer.text.font_system, self.metrics); + buffer.set_text( + &mut state.renderer.text.font_system, + &self.text, + self.attributes.as_attrs(), + Shaping::Basic, + ); + + state.renderer.text.entries.push(TextEntry { + buffer, + position: self.position, + }); + } + + fn id(&self) -> WidgetId { + self.id + } + + fn as_any(&self) -> &dyn Any { + self + } +} @@ -0,0 +1,49 @@ +use std::sync::Arc; +use glam::Vec2; +use winit::event::{ElementState, MouseButton, WindowEvent}; +use winit::window::Window; + +pub struct KinoWinitWindowing { + // mouse + pub mouse_position: Vec2, + pub mouse_button: MouseButton, + pub mouse_press_state: ElementState, + + // keyboard + + // windowing + _window: Arc<Window>, + /// The top-left most pixel + pub viewport_offset: Vec2, +} + +impl KinoWinitWindowing { + /// Creates a new instance of [KinoWinitWindowing] with a specified viewport texture offset. + pub fn new(window: Arc<Window>) -> Self { + Self { + mouse_position: Default::default(), + mouse_button: MouseButton::Left, + mouse_press_state: ElementState::Released, + _window: window, + viewport_offset: Default::default(), + } + } + + pub(crate) fn handle_event(&mut self, event: &WindowEvent) { + match event { + WindowEvent::CursorMoved { position, .. } => { + let screen_pos = Vec2::new(position.x as f32, position.y as f32); + self.mouse_position = screen_pos - self.viewport_offset; + } + WindowEvent::MouseInput { state, button, .. } => { + self.mouse_button = *button; + self.mouse_press_state = *state; + } + WindowEvent::Resized(_) => {} + WindowEvent::KeyboardInput { .. } => {} + WindowEvent::MouseWheel { .. } => {} + WindowEvent::ScaleFactorChanged { .. } => {} + _ => {} + } + } +} @@ -33,6 +33,7 @@ use eucalyptus_core::physics::PhysicsState; use eucalyptus_core::rapier3d::prelude::*; use eucalyptus_core::register_components; use kino_ui::KinoState; +use kino_ui::windowing::KinoWinitWindowing; use kino_ui::rendering::KinoWGPURenderer; mod scene; @@ -219,7 +220,18 @@ impl PlayMode { self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("runtime shader globals"))); self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone())); - self.kino = Some(KinoState::new(KinoWGPURenderer::new(&graphics.device, &graphics.queue, Texture::TEXTURE_FORMAT, [graphics.viewport_texture.size.width as f32, graphics.viewport_texture.size.height as f32]))) + self.kino = Some(KinoState::new( + KinoWGPURenderer::new( + &graphics.device, + &graphics.queue, + Texture::TEXTURE_FORMAT, + [ + graphics.viewport_texture.size.width as f32, + graphics.viewport_texture.size.height as f32, + ], + ), + KinoWinitWindowing::new(graphics.window.clone()), + )) } fn reload_scripts_for_current_world(&mut self) { @@ -190,11 +190,6 @@ 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() { @@ -456,7 +451,10 @@ impl Scene for PlayMode { if !p.is_everything_loaded() && p.is_first_scene { ui.centered_and_justified(|ui| { egui_extras::install_image_loaders(&graphics.get_egui_context()); - ui.image(egui::include_image!("../../../resources/eucalyptus-editor.png")) + ui.add( + egui::Image::new(egui::include_image!("../../../resources/eucalyptus-editor.png")) + .max_width(128.0) + ) }); return; } @@ -498,6 +496,9 @@ impl Scene for PlayMode { ); self.viewport_offset = (image_rect.min.x, image_rect.min.y); + if let Some(kino) = &mut self.kino { + kino.set_viewport_offset(Vec2::new(image_rect.min.x, image_rect.min.y)); + } ui.allocate_exact_size(available_size, egui::Sense::hover()); @@ -533,11 +534,22 @@ impl Scene for PlayMode { }); if let Some(kino) = &mut self.kino { - kino.add_widget(Box::new( + let peter_griffen = kino.add_texture_from_bytes(&graphics.device, &graphics.queue, "theres nothing", include_bytes!("../../../resources/textures/no-texture.png"), 256, 256); + + let rect = kino.add_widget(Box::new( Rectangle::new("rect".into()) + .texture(peter_griffen) )); kino.poll(); + + if kino.response(rect).clicked { + println!("Clicked!"); + }; + + if kino.response(rect).hovering { + println!("Hovering..."); + }; } } else { log::warn!("No such camera exists in the world"); @@ -605,7 +617,7 @@ impl Scene for PlayMode { }); } - if let Err(e) = encoder.submit(graphics.clone()) { + if let Err(e) = encoder.submit() { log_once::error_once!("{}", e); } } @@ -910,7 +922,7 @@ impl Scene for PlayMode { } } - if let Err(e) = encoder.submit(graphics.clone()) { + if let Err(e) = encoder.submit() { log_once::error_once!("{}", e); } @@ -935,7 +947,7 @@ impl Scene for PlayMode { if let Some(kino) = &mut self.kino { let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder")); kino.render(&graphics.device, &graphics.queue, &mut encoder, &graphics.viewport_texture.view); - if let Err(e) = encoder.submit(graphics.clone()) { + if let Err(e) = encoder.submit() { log_once::error_once!("Unable to submit kino: {}", e); } } @@ -949,6 +961,10 @@ impl Scene for PlayMode { yak.handle_window_event(&mut yakui, event); } }); + + if let Some(kino) = &mut self.kino { + kino.handle_event(event); + } } fn exit(&mut self, _event_loop: &ActiveEventLoop) {}