tirbofish/dropbear · diff
feature: text rendering now works (workaround for wgpu 27) + input
feature: contained widgets now work.
feature: shorthand for UI
Signature present but could not be verified.
Unverified
@@ -74,13 +74,13 @@ rapier3d = { version = "0.32", features = [ "simd-stable", "serde-serialize" ] } cbindgen = { version = "0.29.2" } postcard = { version = "1.1", features = ["use-std"]} pollster = "0.4" -yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f", features = ["default-fonts"] } -yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" } -yakui-winit = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" } +#yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f", features = ["default-fonts"] } +#yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" } +#yakui-winit = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" } thiserror = "2.0" tempfile = "3.24" combine = "4.6" -glyphon = "0.8" +glyphon = { git = "https://github.com/grovesNL/glyphon", rev = "9dd9376" } [workspace.dependencies.image] version = "0.25" @@ -44,8 +44,8 @@ dashmap.workspace = true typetag.workspace = true postcard.workspace = true pollster.workspace = true -yakui-wgpu.workspace = true -yakui.workspace = true +#yakui-wgpu.workspace = true +#yakui.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -32,8 +32,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, + // pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>, + // pub yakui_texture: yakui::TextureId, } impl SharedGraphicsContext { @@ -76,8 +76,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(), + // yakui_renderer: state.yakui_renderer.clone(), + // yakui_texture: state.yakui_texture.clone(), surface_config: state.config.clone(), } } @@ -98,8 +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, + // pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>, + // pub yakui_texture: yakui::TextureId, } impl State { @@ -333,18 +333,18 @@ 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_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 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), @@ -373,8 +373,8 @@ Hardware: light_cube_bind_group_layout, }), supports_storage: supports_storage_resources, - yakui_renderer, - yakui_texture, + // yakui_renderer, + // yakui_texture, }; Ok(result) @@ -0,0 +1,158 @@ +// use crate::texture::Texture; +// +// pub struct HdrPipeline { +// pipeline: wgpu::RenderPipeline, +// bind_group: wgpu::BindGroup, +// texture: Texture, +// width: u32, +// height: u32, +// format: wgpu::TextureFormat, +// layout: wgpu::BindGroupLayout, +// } +// +// impl HdrPipeline { +// pub fn new(device: &wgpu::Device, config: &wgpu::SurfaceConfiguration) -> Self { +// let width = config.width; +// let height = config.height; +// +// // We could use `Rgba32Float`, but that requires some extra +// // features to be enabled for rendering. +// let format = wgpu::TextureFormat::Rgba16Float; +// +// let texture = Texture( +// device, +// width, +// height, +// format, +// wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT, +// wgpu::FilterMode::Nearest, +// Some("Hdr::texture"), +// ); +// +// let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { +// label: Some("Hdr::layout"), +// entries: &[ +// // This is the HDR texture +// wgpu::BindGroupLayoutEntry { +// binding: 0, +// visibility: wgpu::ShaderStages::FRAGMENT, +// ty: wgpu::BindingType::Texture { +// sample_type: wgpu::TextureSampleType::Float { filterable: true }, +// view_dimension: wgpu::TextureViewDimension::D2, +// multisampled: false, +// }, +// count: None, +// }, +// wgpu::BindGroupLayoutEntry { +// binding: 1, +// visibility: wgpu::ShaderStages::FRAGMENT, +// ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), +// count: None, +// }, +// ], +// }); +// let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { +// label: Some("Hdr::bind_group"), +// layout: &layout, +// entries: &[ +// wgpu::BindGroupEntry { +// binding: 0, +// resource: wgpu::BindingResource::TextureView(&texture.view), +// }, +// wgpu::BindGroupEntry { +// binding: 1, +// resource: wgpu::BindingResource::Sampler(&texture.sampler), +// }, +// ], +// }); +// +// // We'll cover the shader next +// let shader = wgpu::include_wgsl!("hdr.wgsl"); +// let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { +// label: None, +// bind_group_layouts: &[&layout], +// push_constant_ranges: &[], +// }); +// +// let pipeline = create_render_pipeline( +// device, +// &pipeline_layout, +// config.format.add_srgb_suffix(), +// None, +// // We'll use some math to generate the vertex data in +// // the shader, so we don't need any vertex buffers +// &[], +// wgpu::PrimitiveTopology::TriangleList, +// shader, +// ); +// +// Self { +// pipeline, +// bind_group, +// layout, +// texture, +// width, +// height, +// format, +// } +// } +// +// /// Resize the HDR texture +// pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) { +// self.texture = texture::Texture::create_2d_texture( +// device, +// width, +// height, +// wgpu::TextureFormat::Rgba16Float, +// wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT, +// wgpu::FilterMode::Nearest, +// Some("Hdr::texture"), +// ); +// self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { +// label: Some("Hdr::bind_group"), +// layout: &self.layout, +// entries: &[ +// wgpu::BindGroupEntry { +// binding: 0, +// resource: wgpu::BindingResource::TextureView(&self.texture.view), +// }, +// wgpu::BindGroupEntry { +// binding: 1, +// resource: wgpu::BindingResource::Sampler(&self.texture.sampler), +// }, +// ], +// }); +// self.width = width; +// self.height = height; +// } +// +// /// Exposes the HDR texture +// pub fn view(&self) -> &wgpu::TextureView { +// &self.texture.view +// } +// +// /// The format of the HDR texture +// pub fn format(&self) -> wgpu::TextureFormat { +// self.format +// } +// +// /// This renders the internal HDR texture to the [TextureView] +// /// supplied as parameter. +// pub fn process(&self, encoder: &mut wgpu::CommandEncoder, output: &wgpu::TextureView) { +// let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { +// label: Some("Hdr::process"), +// color_attachments: &[Some(wgpu::RenderPassColorAttachment { +// view: &output, +// resolve_target: None, +// ops: Operations { +// load: wgpu::LoadOp::Load, +// store: wgpu::StoreOp::Store, +// }, +// })], +// depth_stencil_attachment: None, +// }); +// pass.set_pipeline(&self.pipeline); +// pass.set_bind_group(0, &self.bind_group, &[]); +// pass.draw(0..3, 0..1); +// } +// } @@ -5,6 +5,7 @@ use crate::shader::Shader; pub mod shader; pub mod light_cube; pub mod globals; +mod hdr; pub use globals::{Globals, GlobalsUniform}; @@ -1,4 +1,4 @@ -#![cfg_attr(not(debug), windows_subsystem = "windows")] +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use app_dirs2::AppInfo; use dropbear_engine::future::FutureQueue; @@ -43,7 +43,7 @@ semver.workspace = true rustc_version_runtime.workspace = true rapier3d.workspace = true bytemuck.workspace = true -yakui.workspace = true +#yakui.workspace = true thiserror.workspace = true combine.workspace = true @@ -1,8 +1,8 @@ -mod button; -mod utils; -mod text; -mod align; -mod checkbox; +// mod button; +// mod utils; +// mod text; +// mod align; +// mod checkbox; use std::any::Any; use std::cell::RefCell; @@ -12,16 +12,16 @@ use ::jni::JNIEnv; use ::jni::objects::JObject; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; -use yakui::{Alignment, MainAxisSize, Yakui}; +// use yakui::{Alignment, MainAxisSize, Yakui}; use dropbear_engine::utils::ResourceReference; use dropbear_macro::SerializableComponent; use dropbear_traits::SerializableComponent; use crate::scripting::jni::utils::{FromJObject}; use crate::scripting::result::DropbearNativeResult; -use crate::ui::align::{AlignParser}; -use crate::ui::button::ButtonParser; -use crate::ui::checkbox::CheckboxParser; -use crate::ui::text::TextParser; +// use crate::ui::align::{AlignParser}; +// use crate::ui::button::ButtonParser; +// use crate::ui::checkbox::CheckboxParser; +// use crate::ui::text::TextParser; thread_local! { pub static UI_CONTEXT: RefCell<UiContext> = RefCell::new(UiContext::new()); @@ -52,7 +52,7 @@ pub trait WidgetParser: Send + Sync { } pub struct UiContext { - pub yakui_state: Mutex<Yakui>, + // pub yakui_state: Mutex<Yakui>, pub instruction_set: Mutex<Vec<UiInstructionType>>, pub widget_states: Mutex<HashMap<i64, WidgetState>>, pub parsers: Mutex<Vec<Box<dyn WidgetParser>>>, @@ -61,88 +61,25 @@ pub struct UiContext { pub fn poll() { UI_CONTEXT.with(|v| { let ctx = v.borrow(); - let mut instructions = ctx.instruction_set.lock(); + // let mut instructions = ctx.instruction_set.lock(); let mut widget_states = ctx.widget_states.lock(); widget_states.clear(); - let current_instructions = instructions.drain(..).collect::<Vec<UiInstructionType>>(); + // let current_instructions = instructions.drain(..).collect::<Vec<UiInstructionType>>(); - let tree = build_tree(current_instructions); + // let tree = build_tree(current_instructions); - yakui::widgets::Align::new(Alignment::TOP_LEFT).show(|| { - yakui::widgets::List::column() - .main_axis_size(MainAxisSize::Max) - .show(|| { - render_tree(tree, &mut widget_states); - }); - }); + // yakui::widgets::Align::new(Alignment::TOP_LEFT).show(|| { + // yakui::widgets::List::column() + // .main_axis_size(MainAxisSize::Max) + // .show(|| { + // render_tree(tree, &mut widget_states); + // }); + // }); }); } -fn build_tree(instructions: Vec<UiInstructionType>) -> Vec<UiNode> { - let mut stack: Vec<UiNode> = Vec::new(); - let mut root = Vec::new(); - - for instruction in instructions { - match &instruction { - UiInstructionType::Containered(container_ty) => { - match container_ty { - ContaineredWidgetType::Start { .. } => { - stack.push(UiNode { - instruction, - children: Vec::new(), - }); - } - ContaineredWidgetType::End { .. } => { - if let Some(node) = stack.pop() { - if let Some(parent) = stack.last_mut() { - parent.children.push(node); - } else { - root.push(node); - } - } - } - } - } - UiInstructionType::Widget(_) => { - let node = UiNode { - instruction, - children: Vec::new(), - }; - - if let Some(parent) = stack.last_mut() { - parent.children.push(node); - } else { - root.push(node); - } - } - } - } - - root -} - -pub fn render_tree(nodes: Vec<UiNode>, widget_state: &mut HashMap<i64, WidgetState>) { - for node in nodes { - match node.instruction { - UiInstructionType::Containered(container_ty) => { - match container_ty { - ContaineredWidgetType::Start { widget, .. } => { - widget.render(node.children, widget_state); - } - ContaineredWidgetType::End { .. } => { - // already handled in tree building - } - } - } - UiInstructionType::Widget(widget) => { - widget.render(widget_state); - } - } - } -} - #[derive(Debug)] pub enum UiInstructionType { Containered(ContaineredWidgetType), @@ -180,15 +117,15 @@ impl UiContext { pub fn new() -> Self { let mut parsers: Vec<Box<dyn WidgetParser>> = Vec::new(); - parsers.push(Box::new(ButtonParser)); - parsers.push(Box::new(TextParser)); - parsers.push(Box::new(AlignParser)); - parsers.push(Box::new(CheckboxParser)); + // parsers.push(Box::new(ButtonParser)); + // parsers.push(Box::new(TextParser)); + // parsers.push(Box::new(AlignParser)); + // parsers.push(Box::new(CheckboxParser)); - let yakui = Yakui::new(); + // let yakui = Yakui::new(); Self { - yakui_state: Mutex::new(yakui), + // yakui_state: Mutex::new(yakui), instruction_set: Default::default(), widget_states: Default::default(), parsers: Mutex::new(parsers), @@ -5,8 +5,7 @@ supported. ## Other functions -To run a local play test, run `eucalyptus-editor.exe play {project_dir}` with optional jvm args specified through -`--jvm-args {jvm_args}`. +To run a local play test, run `eucalyptus-editor.exe play {project_dir}`. -eucalyptus-editor does not have a scripting interface, therefore can be run without the Java Virtual Machine. If you do +eucalyptus-editor (the editor itself) does not have a scripting interface, therefore can be run without the Java Virtual Machine. If you do wish to enter into "Play Mode" or start testing your game, you will require a form of Java Development Kit (JDK). @@ -3,7 +3,7 @@ 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. +Assembly-like instructions to render different components, including standard and contained widgets. # Example @@ -8,6 +8,14 @@ pub mod asset; pub mod math; pub mod windowing; +pub mod crates { + pub use wgpu; + pub use winit; + pub use glyphon; +} + +pub use widgets::shorthand::*; + use crate::asset::{AssetServer, Handle}; use crate::camera::Camera2D; use crate::rendering::texture::Texture; @@ -15,6 +23,7 @@ use crate::rendering::vertex::Vertex; use crate::rendering::{KinoWGPURenderer}; use crate::resp::WidgetResponse; use crate::widgets::{ContaineredWidget, NativeWidget}; +use crate::math::Rect; use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; use std::fmt::Debug; @@ -23,6 +32,7 @@ use wgpu::{LoadOp, StoreOp}; use rendering::batching::VertexBatch; use crate::windowing::KinoWinitWindowing; use glam::Vec2; +use crate::rendering::text::KinoTextRenderer; /// Holds the state of all the instructions, and the vertices+indices for rendering as well /// as the responses. @@ -34,6 +44,40 @@ pub struct KinoState { assets: AssetServer, batch: PrimitiveBatch, camera: Camera2D, + container_stack: Vec<ContainerContext>, +} + +// public stuff +impl KinoState { + /// Returns a mutable reference to the current [`KinoTextRenderer`], used for text and font + /// management. + pub fn text(&mut self) -> &mut KinoTextRenderer { + &mut self.renderer.text + } + + /// Returns a mutable reference to the current [`KinoWGPURenderer`], used for rendering and + /// pipelines. + pub fn renderer(&mut self) -> &mut KinoWGPURenderer { + &mut self.renderer + } + + /// Returns a mutable reference to the current [`KinoWinitWindowing`], used for handling events + /// and windowing operations. + pub fn windowing(&mut self) -> &mut KinoWinitWindowing { + &mut self.windowing + } + + /// Returns a mutable reference to the current [`Camera2D`], used for displaying the current + /// viewport. + pub fn camera(&mut self) -> &mut Camera2D { + &mut self.camera + } + + /// Returns a mutable reference to the [`AssetServer`], used for storing textures and + /// other assets. + pub fn assets(&mut self) -> &mut AssetServer { + &mut self.assets + } } impl KinoState { @@ -50,6 +94,7 @@ impl KinoState { assets: Default::default(), batch: Default::default(), camera: Camera2D::default(), + container_stack: Default::default(), } } @@ -65,14 +110,22 @@ impl KinoState { /// 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 { - // id: container.id(), - // widget: container, - // } - // )) + pub fn add_container(&mut self, container: Box<dyn ContaineredWidget>) -> WidgetId { + let id = container.id(); + self.instruction_set.push_back(UiInstructionType::Containered( + ContaineredWidgetType::Start { + id, + widget: container, + } + )); + id + } + + /// Ends the current container block. + pub fn end_container(&mut self, id: WidgetId) { + self.instruction_set.push_back(UiInstructionType::Containered( + ContaineredWidgetType::End { id } + )); } /// Adds a [UiInstructionType] to the instruction set. @@ -105,14 +158,20 @@ impl KinoState { /// /// 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 response(&self, id: impl Into<WidgetId>) -> WidgetResponse { + self.widget_states.get(&id.into()).copied().unwrap_or_default() } pub fn set_viewport_offset(&mut self, offset: Vec2) { self.windowing.viewport_offset = offset; } + /// Sets both the viewport offset (top-left in screen space) and scale (screen->viewport). + pub fn set_viewport_transform(&mut self, offset: Vec2, scale: Vec2) { + self.windowing.viewport_offset = offset; + self.windowing.viewport_scale = scale; + } + /// Pushes the vertices and indices to the renderer. /// /// This is the recommended `render()` function and is used when you want @@ -135,6 +194,13 @@ impl KinoState { ); let batch = self.batch.take(); + let (width, height) = { + let tex = view.texture(); + (tex.width(), tex.height()) + }; + + self.renderer.text.prepare(&device, &queue, width, height); + { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("kino render pass"), @@ -160,7 +226,7 @@ impl KinoState { self.renderer.draw_batch(&mut pass, device, queue, &mut tg.batch, texture); } - // self.renderer.text.render(&mut pass); + self.renderer.text.render(&mut pass); } } @@ -170,11 +236,11 @@ impl KinoState { /// `kino_ui` to only draw the widgets. /// /// This sits inside your `render()` loop. - pub fn render_into_pass( - &mut self, + pub fn render_into_pass<'a>( + &'a mut self, device: &wgpu::Device, queue: &wgpu::Queue, - pass: &mut wgpu::RenderPass<'_>, + pass: &mut wgpu::RenderPass<'a>, ) { self.renderer.upload_camera_matrix( queue, @@ -192,7 +258,7 @@ impl KinoState { self.renderer.draw_batch(pass, device, queue, &mut tg.batch, texture); } - // self.renderer.text.render(&mut pass); + self.renderer.text.render(pass); } /// Handles the event into the internal input state. @@ -202,12 +268,6 @@ impl KinoState { 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 @@ -274,7 +334,7 @@ impl KinoState { } /// Returns a reference to the windowing. Used for input detection. - pub fn input(&self) -> &KinoWinitWindowing { + pub(crate) fn input(&self) -> &KinoWinitWindowing { &self.windowing } @@ -282,6 +342,37 @@ impl KinoState { self.widget_states.insert(id, response); } + pub(crate) fn layout_offset(&self) -> Vec2 { + self.container_stack + .last() + .map(|ctx| ctx.offset) + .unwrap_or(Vec2::ZERO) + } + + pub(crate) fn clip_contains(&self, point: Vec2) -> bool { + match self.container_stack.last().and_then(|ctx| ctx.clip) { + Some(rect) => rect.contains(point), + None => self.container_stack.is_empty(), + } + } + + pub(crate) fn push_container(&mut self, rect: Rect) { + let parent_offset = self.layout_offset(); + let world_rect = Rect::new(rect.position + parent_offset, rect.size); + let clip = match self.container_stack.last().and_then(|ctx| ctx.clip) { + Some(parent_clip) => intersect_rects(parent_clip, world_rect), + None => Some(world_rect), + }; + self.container_stack.push(ContainerContext { + offset: world_rect.position, + clip, + }); + } + + pub(crate) fn pop_container(&mut self) { + self.container_stack.pop(); + } + fn build_tree(instructions: Vec<UiInstructionType>) -> Vec<UiNode> { let mut stack: Vec<UiNode> = Vec::new(); let mut root = Vec::new(); @@ -325,7 +416,7 @@ impl KinoState { root } - fn render_tree(&mut self, nodes: Vec<UiNode>) { + pub(crate) fn render_tree(&mut self, nodes: Vec<UiNode>) { for node in nodes { match node.instruction { UiInstructionType::Containered(container_ty) => { @@ -353,6 +444,12 @@ impl KinoState { #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] pub struct WidgetId(u64); +impl Default for WidgetId { + fn default() -> Self { + WidgetId(0) // dummy value + } +} + impl WidgetId { /// Creates a new [`WidgetId`] from an object that can be hashed. pub fn new<H: Hash>(value: H) -> Self { @@ -422,6 +519,12 @@ pub struct UiNode { pub children: Vec<UiNode>, } +#[derive(Clone, Copy, Debug)] +struct ContainerContext { + offset: Vec2, + clip: Option<Rect>, +} + #[derive(Debug, Default)] pub struct TexturedGeometry { pub texture_id: Option<Handle<Texture>>, @@ -451,4 +554,14 @@ impl PrimitiveBatch { pub(crate) fn take(&mut self) -> Vec<TexturedGeometry> { std::mem::take(&mut self.geometry) } +} + +fn intersect_rects(a: Rect, b: Rect) -> Option<Rect> { + let min = a.min().max(b.min()); + let max = a.max().min(b.max()); + if max.cmpge(min).all() { + Some(Rect::new(min, max - min)) + } else { + None + } } @@ -3,22 +3,22 @@ use wgpu::util::{BufferInitDescriptor, DeviceExt}; use batching::VertexBatch; use crate::camera::{CameraRendering, CameraUniform}; use crate::rendering::pipeline::KinoRendererPipeline; -// use crate::rendering::text::KinoTextRenderer; +use crate::rendering::text::KinoTextRenderer; pub mod pipeline; pub mod texture; pub mod vertex; pub mod batching; -// pub mod text; +pub mod text; pub struct KinoWGPURenderer { pipeline: KinoRendererPipeline, default_texture: texture::Texture, pub format: wgpu::TextureFormat, pub size: Vec2, - // pub text: KinoTextRenderer, + pub text: KinoTextRenderer, - camera: CameraRendering, + pub camera: CameraRendering, } impl KinoWGPURenderer { @@ -55,7 +55,7 @@ impl KinoWGPURenderer { }); let default_texture = texture::Texture::create_default(&device, &queue, &pipeline.texture_bind_group_layout, surface_format); - // let text = KinoTextRenderer::new(&device, &queue, surface_format); + let text = KinoTextRenderer::new(&device, &queue, surface_format); log::debug!("Created KinoWGPURenderer"); Self { @@ -63,7 +63,7 @@ impl KinoWGPURenderer { default_texture, format: surface_format, size: Vec2::from_array(size), - // text, + text, camera: CameraRendering { buffer: camera_buffer, bind_group: camera_bind_group, @@ -7,8 +7,7 @@ pub struct TextEntry { } pub struct KinoTextRenderer { - pub font_system: - FontSystem, + pub font_system: FontSystem, pub swash_cache: SwashCache, pub atlas: TextAtlas, pub renderer: TextRenderer, @@ -48,7 +47,8 @@ impl KinoTextRenderer { } /// Prepare the text renderer for drawing - pub(crate) fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) { + pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) { + self.viewport.update(queue, Resolution { width, height }); if self.entries.is_empty() { return; } @@ -85,13 +85,13 @@ impl KinoTextRenderer { self.entries.clear(); } - pub(crate) fn render<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) { + pub 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) { + pub fn resize(&mut self, width: u32, height: u32, queue: &wgpu::Queue) { self.viewport.update(queue, Resolution { width, height }); } } @@ -1,5 +1,8 @@ +use crate::{WidgetId}; + #[derive(Clone, Copy, Debug, Default)] pub struct WidgetResponse { + pub queried: WidgetId, pub clicked: bool, pub hovering: bool, } @@ -1,11 +1,16 @@ pub mod rect; -// pub mod text; +pub mod shorthand; +pub mod text; use std::any::Any; use crate::{KinoState, UiNode, WidgetId}; +/// Determines how the object is anchored. pub enum Anchor { + /// A center anchor is when the position is based on the center of the object (such as the + /// center of a circle) Center, + /// A top left anchor is when the position is based on the top left corner of the rectangle. TopLeft, } @@ -2,12 +2,12 @@ use std::any::Any; use glam::{vec2, Mat2, Vec2}; -use crate::{KinoState, WidgetId}; +use crate::{KinoState, UiNode, WidgetId}; use crate::asset::Handle; use crate::math::Rect; use crate::rendering::texture::Texture; use crate::rendering::vertex::Vertex; -use crate::widgets::{Anchor, Border, Fill, NativeWidget}; +use crate::widgets::{Anchor, Border, ContaineredWidget, Fill, NativeWidget}; use crate::resp::WidgetResponse; use winit::event::{ElementState, MouseButton}; @@ -82,16 +82,19 @@ impl Rectangle { } /// Sets the anchor point of the rectangle + /// /// Defaults to [`Anchor::TopLeft`]. pub fn anchor(mut self, anchor: Anchor) -> Self { self.anchor = anchor; self } + /// Sets the world-space position of the rectangle pub fn at(mut self, position: impl Into<Vec2>) -> Self { self.position = position.into(); self } + /// Sets the size of the rectangle pub fn size(mut self, size: Vec2) -> Self { self.size = size; @@ -124,37 +127,46 @@ impl Rectangle { } /// Custom UV coordinates + /// /// Defaults to covering the full texture ((0,0) - (1,1)) pub fn uv(mut self, coords: [[f32; 2]; 4]) -> Self { self.uvs = coords; self } -} -impl NativeWidget for Rectangle { - fn render(self: Box<Self>, state: &mut KinoState) { + pub fn build(self) -> Box<Self> { + Box::new(self) + } + + fn compute_rect(&self, state: &KinoState) -> (Vec2, Rect, Mat2) { + let container_offset = state.layout_offset(); let offset = match self.anchor { Anchor::TopLeft => Vec2::ZERO, Anchor::Center => -self.size / 2.0, }; - let top_left = self.position + offset; + let local_top_left = self.position + offset; + let top_left = local_top_left + container_offset; let rect = Rect::new(top_left, self.size); + let rot = Mat2::from_angle(self.rotation); + (local_top_left, rect, rot) + } + fn render_body(&self, state: &mut KinoState, rect: &Rect, rot: Mat2) { let input = state.input(); - let hovering = rect.contains(input.mouse_position); + let hovering = rect.contains(input.mouse_position) + && state.clip_contains(input.mouse_position); let clicked = hovering && input.mouse_button == MouseButton::Left && input.mouse_press_state == ElementState::Pressed; state.set_response( self.id, WidgetResponse { + queried: self.id, clicked, hovering, }, ); - let rot = Mat2::from_angle(self.rotation); - let fill_verts: Vec<_> = rect .corners() .iter() @@ -170,12 +182,12 @@ impl NativeWidget for Rectangle { if let Some(border) = self.border { let half_width = border.width / 2.0; let outer_rect = Rect::new( - top_left - Vec2::splat(half_width), - self.size + Vec2::splat(border.width) + rect.position - Vec2::splat(half_width), + rect.size + Vec2::splat(border.width) ); let inner_rect = Rect::new( - top_left + Vec2::splat(half_width), - self.size - Vec2::splat(border.width) + rect.position + Vec2::splat(half_width), + rect.size - Vec2::splat(border.width) ); let outer_corners = outer_rect.corners(); @@ -200,6 +212,31 @@ impl NativeWidget for Rectangle { state.batch.push(&border_verts, &border_indices, None); } } +} + +impl NativeWidget for Rectangle { + fn render(self: Box<Self>, state: &mut KinoState) { + let (_local_top_left, rect, rot) = self.compute_rect(state); + self.render_body(state, &rect, rot); + } + + fn id(&self) -> WidgetId { + self.id + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl ContaineredWidget for Rectangle { + fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) { + let (local_top_left, rect, rot) = self.compute_rect(state); + self.render_body(state, &rect, rot); + state.push_container(Rect::new(local_top_left, self.size)); + state.render_tree(children); + state.pop_container(); + } fn id(&self) -> WidgetId { self.id @@ -0,0 +1,45 @@ +use crate::{KinoState, WidgetId}; +use crate::widgets::rect::Rectangle; +use crate::widgets::text::Text; + +/// Shorthand for a standard rectangle widget. +pub fn rect<F>(kino: &mut KinoState, id: impl Into<WidgetId>, configure: F) -> WidgetId +where + F: FnOnce(&mut Rectangle), +{ + let id = id.into(); + let mut rect = Rectangle::new(id); + configure(&mut rect); + kino.add_widget(Box::new(rect)); + id +} + +/// Shorthand for a rectangle container. +/// +/// `configure` sets up the rectangle, and `contents` emits child widgets between +/// start/end instructions. +pub fn rect_container<C>( + kino: &mut KinoState, + rect: Rectangle, + contents: C, +) -> WidgetId +where + C: FnOnce(&mut KinoState), +{ + let id = rect.id; + + kino.add_container(Box::new(rect)); + contents(kino); + kino.end_container(id); + id +} + +/// Shorthand for a standard label. +pub fn label<F>(kino: &mut KinoState, text: impl ToString, configure: F) -> WidgetId +where + F: FnOnce(&mut Text), +{ + let mut text = Text::new(text); + configure(&mut text); + kino.add_widget(Box::new(text)) +} @@ -2,26 +2,37 @@ use std::any::Any; use glam::Vec2; use glyphon::{Attrs, AttrsOwned, Buffer, Color, Metrics, Shaping}; use crate::{KinoState, WidgetId}; +use crate::math::Rect; use crate::rendering::text::TextEntry; +use crate::resp::WidgetResponse; use crate::widgets::NativeWidget; +use winit::event::{ElementState, MouseButton}; +/// Creates a label with the specified text and properties. +/// +/// # Input +/// Responses are weird for text, as it recognises the input when you touch the text itself. +/// +/// If you want an area, you might be interested in [`crate::rect_container`] (with a transparent colour). pub struct Text { pub id: WidgetId, pub text: String, pub position: Vec2, + pub size: 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 { + pub fn new(text: impl ToString) -> Self { Self { - id: WidgetId::from_str(&text), - text, + id: text.to_string().into(), + text: text.to_string(), position: Vec2::new(10.0, 10.0), + size: Vec2::ZERO, metrics: Metrics::new(16.0, 1.0), - attributes: AttrsOwned::new(Attrs::new()), + attributes: AttrsOwned::new(&Attrs::new().color(Color::rgb(0, 0, 0))), } } @@ -37,6 +48,13 @@ impl Text { self } + /// Sets the position & size from a [`Rect`]. + pub fn with(mut self, rect: &Rect) -> Self { + self.position = rect.position; + self.size = rect.size; + self + } + pub fn with_attrs(mut self, attributes: AttrsOwned) -> Self { self.attributes = attributes; self @@ -46,21 +64,65 @@ impl Text { 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); + if self.size != Vec2::ZERO { + buffer.set_size( + &mut state.renderer.text.font_system, + Some(self.size.x), + Some(self.size.y), + ); + } buffer.set_text( &mut state.renderer.text.font_system, &self.text, - self.attributes.as_attrs(), + &self.attributes.as_attrs(), Shaping::Basic, ); + let mut max_x = 0.0f32; + let mut max_y = 0.0f32; + for run in buffer.layout_runs() { + if let Some(last) = run.glyphs.last() { + max_x = max_x.max(last.x + last.w); + } + max_y = max_y.max(run.line_top + run.line_height); + } + let intrinsic_size = Vec2::new( + max_x.max(1.0), + max_y.max(self.metrics.line_height), + ); + let size = if self.size == Vec2::ZERO { + intrinsic_size + } else { + self.size + }; + + let top_left = self.position + state.layout_offset(); + let rect = Rect::new(top_left, size); + + let input = state.input(); + let hovering = rect.contains(input.mouse_position) + && state.clip_contains(input.mouse_position); + let clicked = hovering + && input.mouse_button == MouseButton::Left + && input.mouse_press_state == ElementState::Pressed; + state.set_response( + self.id, + WidgetResponse { + queried: self.id, + clicked, + hovering, + }, + ); + state.renderer.text.entries.push(TextEntry { buffer, - position: self.position, + position: top_left, }); } @@ -15,6 +15,8 @@ pub struct KinoWinitWindowing { _window: Arc<Window>, /// The top-left most pixel pub viewport_offset: Vec2, + /// Scale from screen-space to viewport texture space + pub viewport_scale: Vec2, } impl KinoWinitWindowing { @@ -26,6 +28,7 @@ impl KinoWinitWindowing { mouse_press_state: ElementState::Released, _window: window, viewport_offset: Default::default(), + viewport_scale: Vec2::ONE, } } @@ -33,7 +36,8 @@ impl KinoWinitWindowing { 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; + let local = screen_pos - self.viewport_offset; + self.mouse_position = local * self.viewport_scale; } WindowEvent::MouseInput { state, button, .. } => { self.mouse_button = *button; @@ -29,13 +29,13 @@ serde.workspace = true crossbeam-channel.workspace = true gilrs.workspace = true futures.workspace = true -yakui-winit.workspace = true -yakui.workspace = true egui.workspace = true egui_extras.workspace = true glam.workspace = true wgpu.workspace = true -yakui-wgpu.workspace = true +#yakui-winit.workspace = true +#yakui.workspace = true +#yakui-wgpu.workspace = true [features] debug = [] @@ -5,7 +5,6 @@ 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) { @@ -32,57 +31,14 @@ 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, - }); - } - }); } } @@ -27,7 +27,7 @@ use std::collections::HashMap; use std::path::PathBuf; use wgpu::SurfaceConfiguration; use winit::window::Fullscreen; -use yakui_winit::YakuiWinit; +// use yakui_winit::YakuiWinit; use dropbear_engine::texture::Texture; use eucalyptus_core::physics::PhysicsState; use eucalyptus_core::rapier3d::prelude::*; @@ -149,7 +149,7 @@ pub struct PlayMode { viewport_offset: (f32, f32), // ui - yakui_winit: Option<YakuiWinit>, + // yakui_winit: Option<YakuiWinit>, kino: Option<kino_ui::KinoState>, } @@ -197,7 +197,7 @@ impl PlayMode { collision_event_receiver: Some(ce_r), collision_force_event_receiver: Some(cfe_r), event_collector, - yakui_winit: None, + // yakui_winit: None, display_settings: DisplaySettings { window_mode: WindowMode::Windowed, maintain_aspect_ratio: true, @@ -33,15 +33,15 @@ use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOAD use crate::PlayMode; use eucalyptus_core::physics::collider::shader::create_wireframe_geometry; use eucalyptus_core::ui::UI_CONTEXT; -use kino_ui::widgets::{Border, Fill}; +use kino_ui::widgets::{Anchor, Border, Fill}; use kino_ui::widgets::rect::Rectangle; impl Scene for PlayMode { fn load(&mut self, graphics: Arc<SharedGraphicsContext>) { - let mut yak = yakui_winit::YakuiWinit::new(&graphics.window); - yak.set_automatic_viewport(false); - yak.set_automatic_scale_factor(false); - self.yakui_winit = Some(yak); + // let mut yak = yakui_winit::YakuiWinit::new(&graphics.window); + // yak.set_automatic_viewport(false); + // yak.set_automatic_scale_factor(false); + // self.yakui_winit = Some(yak); if self.current_scene.is_none() { let initial_scene = if let Some(s) = &self.initial_scene { @@ -493,7 +493,20 @@ 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)); + let scale_x = if display_width > 0.0 { + graphics.viewport_texture.size.width as f32 / display_width + } else { + 1.0 + }; + let scale_y = if display_height > 0.0 { + graphics.viewport_texture.size.height as f32 / display_height + } else { + 1.0 + }; + kino.set_viewport_transform( + Vec2::new(image_rect.min.x, image_rect.min.y), + Vec2::new(scale_x, scale_y), + ); } ui.allocate_exact_size(available_size, egui::Sense::hover()); @@ -506,31 +519,31 @@ impl Scene for PlayMode { }); // overlay - UI_CONTEXT.with(|yakui_cell| { - let yak = yakui_cell.borrow(); - let mut yakui = yak.yakui_state.lock(); - - let tex_size = graphics.viewport_texture.size; - let viewport_size = yakui::geometry::Vec2::new( - tex_size.width as f32, - tex_size.height as f32, - ); - yakui.set_surface_size(viewport_size); - yakui.set_unscaled_viewport(yakui::geometry::Rect::from_pos_size( - yakui::geometry::Vec2::ZERO, - viewport_size, - )); - yakui.set_scale_factor(graphics.window.scale_factor() as f32); - - yakui.start(); - - // eucalyptus_core::ui::poll(); - - yakui.finish(); - }); + // UI_CONTEXT.with(|yakui_cell| { + // let yak = yakui_cell.borrow(); + // let mut yakui = yak.yakui_state.lock(); + // + // let tex_size = graphics.viewport_texture.size; + // let viewport_size = yakui::geometry::Vec2::new( + // tex_size.width as f32, + // tex_size.height as f32, + // ); + // yakui.set_surface_size(viewport_size); + // yakui.set_unscaled_viewport(yakui::geometry::Rect::from_pos_size( + // yakui::geometry::Vec2::ZERO, + // viewport_size, + // )); + // yakui.set_scale_factor(graphics.window.scale_factor() as f32); + // + // yakui.start(); + // + // // eucalyptus_core::ui::poll(); + // + // yakui.finish(); + // }); if let Some(kino) = &mut self.kino { - #[allow(dead_code)] + // #[allow(dead_code)] let no_texture = kino.add_texture_from_bytes( &graphics.device, &graphics.queue, "no texture", @@ -538,22 +551,52 @@ impl Scene for PlayMode { 256, 256 ); - let rect = kino.add_widget(Box::new( - Rectangle::new("rect") - .texture(no_texture) - .size(vec2(128.0, 100.0)) - .border(Border::new([1.0, 1.0, 1.0, 1.0], 3.0)) - .fill(Fill::new([1.0, 1.0, 1.0, 0.5])), - )); + let parent = kino_ui::rect_container( + kino, + Rectangle::new("parent") + .fill(Fill::new([1.0, 1.0, 1.0, 1.0])) + .size(vec2(400.0, 400.0)), + |kino| { + kino.add_widget(Rectangle::new("rect") + .texture(no_texture) + .size(vec2(128.0, 100.0)) + .border(Border::new([1.0, 0.0, 0.0, 1.0], 3.0)) + .fill(Fill::new([1.0, 1.0, 1.0, 1.0])) + .texture(no_texture) + .build() + ); + } + ); + + kino_ui::label(kino, "Hello World!", |l| { + l.position = vec2(graphics.viewport_texture.size.width as f32 / 2.0, graphics.viewport_texture.size.height as f32 / 2.0); + l.metrics.font_size = 30.0; + }); kino.poll(); - if kino.response(rect).clicked { - println!("Clicked!"); + if kino.response(parent).clicked { + println!("Parent clicked!"); + }; + + // if kino.response(parent).hovering { + // println!("Parent hovering"); + // }; + + if kino.response("rect").clicked { + println!("child clicked!"); }; - if kino.response(rect).hovering { - println!("Hovering..."); + // if kino.response("rect").hovering { + // println!("child hovering"); + // }; + + if kino.response("Hello World!").clicked { + println!("text clicked") + } + + if kino.response("Hello World!").hovering { + println!("text hovering"); }; } } else { @@ -959,13 +1002,13 @@ impl Scene for PlayMode { } fn handle_event(&mut self, event: &WindowEvent) { - UI_CONTEXT.with(|yakui_cell| { - let yak = yakui_cell.borrow(); - let mut yakui = yak.yakui_state.lock(); - if let Some(yak) = &mut self.yakui_winit { - yak.handle_window_event(&mut yakui, event); - } - }); + // UI_CONTEXT.with(|yakui_cell| { + // let yak = yakui_cell.borrow(); + // let mut yakui = yak.yakui_state.lock(); + // if let Some(yak) = &mut self.yakui_winit { + // yak.handle_window_event(&mut yakui, event); + // } + // }); if let Some(kino) = &mut self.kino { kino.handle_event(event); @@ -40,7 +40,8 @@ slank::CompiledSlangShader::from_bytes("shader_label", shader_bytes); There are two main features currently available: - `download-slang` - Slank downloads the latest slangc compiler from the GitHub releases and stores it - in the user cache. The SLANG_DIR will be set to the directory of the slangc compiler. + in the user cache. The SLANG_DIR will be set to the directory of the slangc compiler. + Note: Using this in CI will require you to add a `GITHUB_TOKEN` env var to avoid the rate limits. Locally, you are fine. - `use-wgpu` - Enables wgpu as a dependency and unlocks utility traits for wgpu. # Contribution @@ -1,8 +1,5 @@ -use std::fs::File; use std::path::PathBuf; use std::process::Command; -use flate2::read::GzDecoder; -use tar::Archive; fn main() -> anyhow::Result<()> { println!("cargo:rerun-if-changed=build.rs"); @@ -254,7 +251,7 @@ fn download_file(url: &str, dest: &PathBuf) -> anyhow::Result<()> { return Err(anyhow::anyhow!("Download failed with status: {}", response.status())); } - let mut file = File::create(dest) + let mut file = std::fs::File::create(dest) .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; response.copy_to(&mut file) @@ -265,7 +262,7 @@ fn download_file(url: &str, dest: &PathBuf) -> anyhow::Result<()> { #[cfg(feature = "download-slang")] fn extract_archive(archive: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { - let file = File::open(archive) + let file = std::fs::File::open(archive) .map_err(|e| anyhow::anyhow!("Failed to open archive: {}", e))?; if archive.extension().and_then(|s| s.to_str()) == Some("zip") { @@ -275,8 +272,8 @@ fn extract_archive(archive: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { archive.extract(dest) .map_err(|e| anyhow::anyhow!("Failed to extract zip: {}", e))?; } else { - let tar = GzDecoder::new(file); - let mut archive = Archive::new(tar); + let tar = flate2::read::GzDecoder::new(file); + let mut archive = tar::Archive::new(tar); archive.unpack(dest) .map_err(|e| anyhow::anyhow!("Failed to extract tar.gz: {}", e))?; @@ -578,6 +578,6 @@ mod tests { let bytes = vec![1, 2, 3, 4]; let shader = CompiledSlangShader::from_bytes("idk", &bytes); assert_eq!(shader.source, bytes); - assert_eq!(shader.label(), "unknown"); + assert_eq!(shader.label(), "idk"); } }