From aff9c2a75cdca93c7dd6ca1a8c28d0a6a1fc4a3e Mon Sep 17 00:00:00 2001 From: lennlouisgeek Date: Mon, 18 May 2026 02:17:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20app/theme/ui/matri?= =?UTF-8?q?x/render=20=E6=A8=A1=E5=9D=97=EF=BC=8C=E9=87=8D=E6=9E=84=20shad?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.rs | 89 ++++++++ src/main.rs | 545 +----------------------------------------------- src/matrix.rs | 121 +++++++++++ src/render.rs | 333 +++++++++++++++++++++++++++++ src/shader.wgsl | 205 ++++++++++++++---- src/theme.rs | 114 ++++++++++ src/ui.rs | 116 +++++++++++ 7 files changed, 947 insertions(+), 576 deletions(-) create mode 100644 src/app.rs create mode 100644 src/matrix.rs create mode 100644 src/render.rs create mode 100644 src/theme.rs create mode 100644 src/ui.rs diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..37b7535 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,89 @@ +use std::time::Instant; + +use eframe::{egui, egui_wgpu}; + +use crate::theme::{ENGINEERING_DARK, apply_theme}; +use crate::{ + matrix::{MATRIX_COLS, MATRIX_ROWS}, + render::{BackgroundRenderResources, WgpuBackgroundCallback}, + ui::{FloatingPanelState, draw_config_panel, draw_scene_panel, draw_stats_panel}, +}; + +pub struct EskinDesktopApp { + scene_panel: FloatingPanelState, + config_panel: FloatingPanelState, + stats_panel: FloatingPanelState, + started_at: Instant, +} + +impl EskinDesktopApp { + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + apply_theme(&cc.egui_ctx, &ENGINEERING_DARK); + + let wgpu_state = cc + .wgpu_render_state + .as_ref() + .expect("need open eframe wgpu renderer feature"); + + let mut renderer = wgpu_state.renderer.write(); + renderer + .callback_resources + .insert(BackgroundRenderResources::new( + &wgpu_state.device, + &wgpu_state.target_format, + MATRIX_ROWS, + MATRIX_COLS, + )); + + Self { + scene_panel: FloatingPanelState::new([16.0, 48.0], [16.0, 48.0]), + config_panel: FloatingPanelState::new([840.0, 48.0], [128.0, 48.0]), + stats_panel: FloatingPanelState::new([16.0, 520.0], [240.0, 48.0]), + started_at: Instant::now(), + } + } + + fn draw_wgpu_background(&mut self, ui: &mut egui::Ui) { + let rect = ui.max_rect(); + let width = rect.width().max(1.0); + let height = rect.height().max(1.0); + + ui.painter().add(egui_wgpu::Callback::new_paint_callback( + rect, + WgpuBackgroundCallback { + width, + height, + time: self.started_at.elapsed().as_secs_f32(), + }, + )); + } + + fn draw_toolbar(&mut self, ui: &mut egui::Ui) { + egui::Panel::top("main_menu").show_inside(ui, |ui| { + ui.horizontal(|ui| { + ui.checkbox(&mut self.scene_panel.visible, "Scene"); + ui.checkbox(&mut self.config_panel.visible, "Config"); + ui.checkbox(&mut self.stats_panel.visible, "Stats"); + }); + }); + } + + fn draw_floating_panels(&mut self, ctx: &egui::Context) { + draw_scene_panel(ctx, &mut self.scene_panel); + draw_config_panel(ctx, &mut self.config_panel); + draw_stats_panel(ctx, &mut self.stats_panel); + } +} + +impl eframe::App for EskinDesktopApp { + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + let ctx = ui.ctx().clone(); + + self.draw_wgpu_background(ui); + self.draw_toolbar(ui); + self.draw_floating_panels(&ctx); + + // Keep repainting while the wgpu background is a realtime viewport. + ctx.request_repaint(); + } +} diff --git a/src/main.rs b/src/main.rs index b8b38a3..c99241c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,11 @@ -pub mod texture; +mod app; +mod matrix; +mod render; +mod theme; +mod ui; + +use app::EskinDesktopApp; -use bytemuck; -use core::f32::consts; -use eframe::{ - egui, - egui_wgpu::{self, wgpu}, - wgpu::util::DeviceExt, -}; -const NUM_INSTANCES_PER_ROW: u32 = 10; -const INSTANCE_DISPLACEMENT: glam::Vec3 = glam::Vec3::new( - NUM_INSTANCES_PER_ROW as f32 * 0.5, - 0.0, - NUM_INSTANCES_PER_ROW as f32 * 0.5, -); fn main() -> eframe::Result<()> { env_logger::init(); @@ -27,527 +20,3 @@ fn main() -> eframe::Result<()> { Box::new(|cc| Ok(Box::new(EskinDesktopApp::new(cc)))), ) } - -struct EskinDesktopApp { - show_scene_panel: bool, - show_config_panel: bool, - show_stats_panel: bool, -} - -impl EskinDesktopApp { - fn new(cc: &eframe::CreationContext<'_>) -> Self { - let wgpu_state = cc - .wgpu_render_state - .as_ref() - .expect("need open eframe wgpu renderer feature"); - - let mut renderer = wgpu_state.renderer.write(); - renderer - .callback_resources - .insert(BackgroundRenderResources::new( - &wgpu_state.device, - &wgpu_state.target_format, - &wgpu_state.queue, - 12, - 7, - )); - - Self { - show_scene_panel: true, - show_config_panel: true, - show_stats_panel: true, - } - } - - fn draw_wgpu_background(&mut self, ui: &mut egui::Ui) { - let rect = ui.max_rect(); - let width = rect.width(); - let height = rect.height(); - - ui.painter().add(egui_wgpu::Callback::new_paint_callback( - rect, - WgpuBackgroundCallback { - aspect: width / height, - }, - )); - } - - fn draw_toolbar(&mut self, ui: &mut egui::Ui) { - egui::Panel::top("main_menu").show_inside(ui, |ui| { - ui.horizontal(|ui| { - ui.checkbox(&mut self.show_scene_panel, "Scene"); - ui.checkbox(&mut self.show_config_panel, "Config"); - ui.checkbox(&mut self.show_stats_panel, "Stats"); - }); - }); - } - - fn draw_floating_panels(&mut self, ctx: &egui::Context) { - egui::Window::new("Scene") - .open(&mut self.show_scene_panel) - .default_pos([16.0, 48.0]) - .show(ctx, |ui| { - ui.label("Models / materials / lights"); - }); - - egui::Window::new("Config") - .open(&mut self.show_config_panel) - .default_pos([840.0, 48.0]) - .show(ctx, |ui| { - ui.label("Render and viewport settings"); - }); - - egui::Window::new("Stats") - .open(&mut self.show_stats_panel) - .default_pos([16.0, 520.0]) - .show(ctx, |ui| { - ui.label("FPS / GPU info"); - }); - } -} - -impl eframe::App for EskinDesktopApp { - fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { - let ctx = ui.ctx().clone(); - - self.draw_wgpu_background(ui); - self.draw_toolbar(ui); - self.draw_floating_panels(&ctx); - - // Keep repainting while the wgpu background is a realtime viewport. - ctx.request_repaint(); - } -} - -struct WgpuBackgroundCallback { - aspect: f32, -} - -impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback { - fn prepare( - &self, - _device: &wgpu::Device, - queue: &wgpu::Queue, - _screen_descriptor: &egui_wgpu::ScreenDescriptor, - _egui_encoder: &mut wgpu::CommandEncoder, - resources: &mut egui_wgpu::CallbackResources, - ) -> Vec { - let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap(); - resources.prepare(queue, self.aspect); - - Vec::new() - } - - fn paint( - &self, - _info: egui::PaintCallbackInfo, - render_pass: &mut wgpu::RenderPass<'static>, - resources: &egui_wgpu::CallbackResources, - ) { - let resources: &BackgroundRenderResources = resources.get().unwrap(); - resources.paint(render_pass); - } -} - -struct BackgroundRenderResources { - camera: Camera, - camera_uniform: CameraUniform, - camera_buffer: wgpu::Buffer, - camera_bind_group: wgpu::BindGroup, - - #[allow(dead_code)] - diffuse_texture: texture::Texture, - diffuse_bind_group: wgpu::BindGroup, - - render_pipeline: wgpu::RenderPipeline, - vertex_buffer: wgpu::Buffer, - index_buffer: wgpu::Buffer, - num_indices: u32, - - instance: Vec, - instance_buffer: wgpu::Buffer, - cols: Option, - rows: Option, -} - -impl BackgroundRenderResources { - fn new( - device: &wgpu::Device, - target_format: &wgpu::TextureFormat, - queue: &wgpu::Queue, - rows: u8, - cols: u8, - ) -> Self { - let diffuse_bytes = include_bytes!("happy-tree.png"); - let diffuse_texture = - texture::Texture::from_bytes(device, queue, diffuse_bytes, "happy-tree.png").unwrap(); - - let texture_group_bind_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("texture_bind_group_layout"), - entries: &[ - 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 diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("texture_bind_group"), - layout: &texture_group_bind_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&diffuse_texture.view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler), - }, - ], - }); - - let camera = Camera { - eye: (0.0, 5.0, 10.0).into(), - target: (0.0, 0.0, 0.0).into(), - up: glam::Vec3::Y, - aspect: 1.0, - fovy: 45.0, - znear: 0.1, - zfar: 100.0, - }; - - let mut camera_uniform = CameraUniform::new(); - camera_uniform.update_view_proj(&camera); - - let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Camera Buffer"), - contents: bytemuck::cast_slice(&[camera_uniform]), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - - let instances = (0..NUM_INSTANCES_PER_ROW) - .flat_map(|z| { - (0..NUM_INSTANCES_PER_ROW).map(move |x| { - let position = glam::Vec3 { - x: x as f32, - y: 0.0, - z: z as f32, - } - INSTANCE_DISPLACEMENT; - - let rotation = if position.length().abs() <= f32::EPSILON { - // this is needed so an object at (0, 0, 0) won't get scaled to zero - // as Quaternions can effect scale if they're not create correctly - glam::Quat::from_axis_angle(glam::Vec3::Z, 0.0) - } else { - glam::Quat::from_axis_angle(position.normalize(), consts::FRAC_PI_2) - }; - - Instance { position, rotation } - }) - }) - .collect::>(); - - let instance_data = instances.iter().map(Instance::to_raw).collect::>(); - let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Instance Buffer"), - contents: bytemuck::cast_slice(&instance_data), - usage: wgpu::BufferUsages::VERTEX, - }); - - let camera_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("camera_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - - let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("camera_bind_group"), - layout: &camera_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: camera_buffer.as_entire_binding(), - }], - }); - - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Shader"), - source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), - }); - - let render_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Render Pipeline Layout"), - bind_group_layouts: &[ - Some(&texture_group_bind_layout), - Some(&camera_bind_group_layout), - ], - immediate_size: 0, - }); - - let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Render Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - compilation_options: Default::default(), - buffers: &[Vertex::desc(), InstanceRaw::desc()], - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - compilation_options: Default::default(), - targets: &[Some(wgpu::ColorTargetState { - format: target_format.clone(), - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent::REPLACE, - alpha: wgpu::BlendComponent::REPLACE, - }), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - // Requires Features::DEPTH_CLIP_CONTROL - unclipped_depth: false, - // Requires Features::CONSERVATIVE_RASTERIZATION - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - - multiview_mask: None, - cache: None, - }); - - let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Vertex Buffer"), - contents: bytemuck::cast_slice(VERTICES), - usage: wgpu::BufferUsages::VERTEX, - }); - - let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Index Buffer"), - contents: bytemuck::cast_slice(INDICES), - usage: wgpu::BufferUsages::INDEX, - }); - - let num_indices = INDICES.len() as u32; - - Self { - camera, - camera_uniform, - camera_buffer, - camera_bind_group, - diffuse_texture, - diffuse_bind_group, - render_pipeline, - vertex_buffer, - index_buffer, - num_indices, - instance: instances, - instance_buffer, - cols: Some(cols), - rows: Some(rows), - } - } - - // fn with_dot_matrix() - - fn prepare(&mut self, queue: &wgpu::Queue, aspect: f32) { - self.camera.aspect = aspect; - self.camera_uniform.update_view_proj(&self.camera); - - queue.write_buffer( - &self.camera_buffer, - 0, - bytemuck::cast_slice(&[self.camera_uniform]), - ); - } - - fn paint(&self, render_pass: &mut wgpu::RenderPass<'_>) { - // TODO: set pipeline / bind groups / buffers and draw the model viewport here. - render_pass.set_pipeline(&self.render_pipeline); - render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]); - render_pass.set_bind_group(1, &self.camera_bind_group, &[]); - render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..)); - render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16); - // UPDATED! - render_pass.draw_indexed(0..self.num_indices, 0, 0..self.instance.len() as _); - } -} - -#[repr(C)] -#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] -struct Vertex { - position: [f32; 3], - tex_coords: [f32; 2], -} - -impl Vertex { - fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { - use core::mem; - wgpu::VertexBufferLayout { - array_stride: mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, - shader_location: 1, - format: wgpu::VertexFormat::Float32x2, - }, - ], - } - } -} - -const VERTICES: &[Vertex] = &[ - Vertex { - position: [-0.0868241, 0.49240386, 0.0], - tex_coords: [0.4131759, 0.00759614], - }, // A - Vertex { - position: [-0.49513406, 0.06958647, 0.0], - tex_coords: [0.0048659444, 0.43041354], - }, // B - Vertex { - position: [-0.21918549, -0.44939706, 0.0], - tex_coords: [0.28081453, 0.949397], - }, // C - Vertex { - position: [0.35966998, -0.3473291, 0.0], - tex_coords: [0.85967, 0.84732914], - }, // D - Vertex { - position: [0.44147372, 0.2347359, 0.0], - tex_coords: [0.9414737, 0.2652641], - }, // E -]; - -const INDICES: &[u16] = &[0, 1, 4, 1, 2, 4, 2, 3, 4]; - -struct Camera { - eye: glam::Vec3, - target: glam::Vec3, - up: glam::Vec3, - aspect: f32, - fovy: f32, - znear: f32, - zfar: f32, -} - -impl Camera { - fn build_view_projection_matrix(&self) -> glam::Mat4 { - let view = glam::Mat4::look_at_rh(self.eye, self.target, self.up); - let projection = - glam::Mat4::perspective_rh(self.fovy.to_radians(), self.aspect, self.znear, self.zfar); - projection * view - } -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct CameraUniform { - view_proj: [[f32; 4]; 4], -} - -impl CameraUniform { - fn new() -> Self { - Self { - view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(), - } - } - - fn update_view_proj(&mut self, camera: &Camera) { - self.view_proj = camera.build_view_projection_matrix().to_cols_array_2d(); - } -} - -struct Instance { - position: glam::Vec3, - rotation: glam::Quat, -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct InstanceRaw { - model: [[f32; 4]; 4], -} - -impl Instance { - fn to_raw(&self) -> InstanceRaw { - InstanceRaw { - model: (glam::Mat4::from_translation(self.position) - * glam::Mat4::from_quat(self.rotation)) - .to_cols_array_2d(), - } - } -} - -impl InstanceRaw { - fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { - use core::mem; - wgpu::VertexBufferLayout { - array_stride: mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Instance, - attributes: &[ - wgpu::VertexAttribute { - offset: 0, - shader_location: 5, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: core::mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, - shader_location: 6, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: core::mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, - shader_location: 7, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: core::mem::size_of::<[f32; 12]>() as wgpu::BufferAddress, - shader_location: 8, - format: wgpu::VertexFormat::Float32x4, - }, - ], - } - } -} diff --git a/src/matrix.rs b/src/matrix.rs new file mode 100644 index 0000000..91fce8a --- /dev/null +++ b/src/matrix.rs @@ -0,0 +1,121 @@ +pub const MATRIX_ROWS: u32 = 12; +pub const MATRIX_COLS: u32 = 7; + +const BASE_MATRIX_SPAN: f32 = 24.0; +const MATRIX_SPAN_GROWTH: f32 = 0.6; +const MIN_MATRIX_SPAN: f32 = 24.0; +const MAX_MATRIX_SPAN: f32 = 58.0; +const MIN_CELL_SPACING: f32 = 0.52; +const MAX_CELL_SPACING: f32 = 3.8; +const MIN_BOARD_PADDING: f32 = 2.6; +const MAX_BOARD_PADDING: f32 = 6.8; +const MATRIX_OFFSET_Y: f32 = -2.4; +const MATRIX_OFFSET_Z: f32 = 12.0; +const HEIGHT_SCALE: f32 = 10.6; +const BASE_HEIGHT: f32 = 0.12; +const CAMERA_FOV: f32 = 36.0; +const CAMERA_DISTANCE_MIN: f32 = 30.0; +const CAMERA_DISTANCE_MAX: f32 = 122.0; +const CAMERA_FIT_PADDING: f32 = 1.04; +const CAMERA_ELEVATION_DEG: f32 = 64.0; +const CAMERA_TARGET_X: f32 = 0.0; +const CAMERA_TARGET_Y: f32 = MATRIX_OFFSET_Y + 0.2; +const CAMERA_TARGET_Z: f32 = MATRIX_OFFSET_Z - 0.4; + +pub struct MatrixLayout { + pub cell_spacing: f32, + pub board_width: f32, + pub board_depth: f32, + pub board_padding: f32, + pub label_float_offset: f32, +} + +impl MatrixLayout { + pub fn new(rows: u32, cols: u32) -> Self { + let longest_edge = rows.max(cols).max(1) as f32; + let edge_span = (longest_edge - 1.0).max(1.0); + let target_span = (BASE_MATRIX_SPAN + edge_span * MATRIX_SPAN_GROWTH) + .clamp(MIN_MATRIX_SPAN, MAX_MATRIX_SPAN); + let cell_spacing = (target_span / edge_span).clamp(MIN_CELL_SPACING, MAX_CELL_SPACING); + let board_width = cols.max(1) as f32 * cell_spacing; + let board_depth = rows.max(1) as f32 * cell_spacing; + let board_padding = (cell_spacing * 1.62).clamp(MIN_BOARD_PADDING, MAX_BOARD_PADDING); + let label_float_offset = (cell_spacing * 0.42).clamp(0.36, 1.12); + + Self { + cell_spacing, + board_width, + board_depth, + board_padding, + label_float_offset, + } + } +} + +pub fn build_view_projection(aspect: f32, layout: &MatrixLayout) -> [[f32; 4]; 4] { + let camera_distance = fit_camera_distance( + layout.board_width, + layout.board_depth, + layout.board_padding, + aspect, + ); + let elevation = CAMERA_ELEVATION_DEG.to_radians(); + let target = glam::Vec3::new(CAMERA_TARGET_X, CAMERA_TARGET_Y, CAMERA_TARGET_Z); + let eye = glam::Vec3::new( + CAMERA_TARGET_X, + CAMERA_TARGET_Y + elevation.sin() * camera_distance, + CAMERA_TARGET_Z + elevation.cos() * camera_distance, + ); + let view = glam::Mat4::look_at_rh(eye, target, glam::Vec3::Y); + let projection = glam::Mat4::perspective_rh(CAMERA_FOV.to_radians(), aspect, 0.1, 500.0); + let open_gl_to_wgpu = glam::Mat4::from_cols_array(&[ + 1.0, 0.0, 0.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 0.5, 0.0, // + 0.0, 0.0, 0.5, 1.0, + ]); + + (open_gl_to_wgpu * projection * view).to_cols_array_2d() +} + +pub fn glyph_world_position( + row: u32, + col: u32, + rows: u32, + cols: u32, + layout: &MatrixLayout, + time: f32, +) -> ([f32; 4], f32) { + let normalized = demo_pressure(row, col, time); + let height = BASE_HEIGHT + normalized.powf(0.9) * HEIGHT_SCALE; + let x = (col as f32 - cols as f32 / 2.0 + 0.5) * layout.cell_spacing; + let z = (row as f32 - rows as f32 / 2.0 + 0.5) * layout.cell_spacing; + + ( + [ + x, + MATRIX_OFFSET_Y + height + layout.label_float_offset, + MATRIX_OFFSET_Z + z, + 1.0, + ], + normalized, + ) +} + +fn demo_pressure(row: u32, col: u32, time: f32) -> f32 { + let seed = ((row * 17 + col * 31) as f32).sin() * 43_758.547; + let phase = seed.fract() * std::f32::consts::TAU; + let slow = 0.5 + 0.5 * (time * 1.35 + phase).sin(); + let row_wave = 0.5 + 0.5 * (time * 0.82 + row as f32 * 0.54 + col as f32 * 0.19).sin(); + (slow * row_wave).powf(0.72).clamp(0.0, 1.0) +} + +fn fit_camera_distance(board_width: f32, board_depth: f32, board_padding: f32, aspect: f32) -> f32 { + let padded_width = board_width + board_padding * 2.0; + let padded_depth = board_depth + board_padding * 2.0; + let safe_aspect = aspect.max(0.5); + let effective_half_span = (padded_depth * 0.5).max((padded_width * 0.5) / safe_aspect); + let fov_radians = (CAMERA_FOV * 0.5).to_radians(); + let fit_distance = (effective_half_span / fov_radians.tan()) * CAMERA_FIT_PADDING; + fit_distance.clamp(CAMERA_DISTANCE_MIN, CAMERA_DISTANCE_MAX) +} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..a0ddd11 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,333 @@ +use eframe::{ + egui, + egui_wgpu::{self, wgpu}, + wgpu::util::DeviceExt, +}; + +use crate::matrix::{MatrixLayout, build_view_projection, glyph_world_position}; + +pub struct WgpuBackgroundCallback { + pub width: f32, + pub height: f32, + pub time: f32, +} + +impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback { + fn prepare( + &self, + _device: &wgpu::Device, + queue: &wgpu::Queue, + _screen_descriptor: &egui_wgpu::ScreenDescriptor, + _egui_encoder: &mut wgpu::CommandEncoder, + resources: &mut egui_wgpu::CallbackResources, + ) -> Vec { + let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap(); + resources.prepare(queue, self.width, self.height, self.time); + + Vec::new() + } + + fn paint( + &self, + _info: egui::PaintCallbackInfo, + render_pass: &mut wgpu::RenderPass<'static>, + resources: &egui_wgpu::CallbackResources, + ) { + let resources: &BackgroundRenderResources = resources.get().unwrap(); + resources.paint(render_pass); + } +} + +pub struct BackgroundRenderResources { + layout: MatrixLayout, + rows: u32, + cols: u32, + uniform: MatrixUniform, + uniform_buffer: wgpu::Buffer, + uniform_bind_group: wgpu::BindGroup, + background_pipeline: wgpu::RenderPipeline, + glyph_pipeline: wgpu::RenderPipeline, + glyph_vertex_buffer: wgpu::Buffer, + glyph_instance_buffer: wgpu::Buffer, + glyph_instances: Vec, +} + +impl BackgroundRenderResources { + pub fn new( + device: &wgpu::Device, + target_format: &wgpu::TextureFormat, + rows: u32, + cols: u32, + ) -> Self { + let layout = MatrixLayout::new(rows, cols); + let uniform = MatrixUniform::new(1.0, 1.0, build_view_projection(1.0, &layout)); + let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Pressure Matrix Uniform Buffer"), + contents: bytemuck::cast_slice(&[uniform]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("pressure_matrix_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("pressure_matrix_bind_group"), + layout: &uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Pressure Matrix Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Pressure Matrix Pipeline Layout"), + bind_group_layouts: &[Some(&uniform_bind_group_layout)], + immediate_size: 0, + }); + + let background_pipeline = + create_background_pipeline(device, target_format, &shader, &pipeline_layout); + let glyph_pipeline = + create_glyph_pipeline(device, target_format, &shader, &pipeline_layout); + + let glyph_vertices = [ + GlyphVertex { + local: [-1.0, -1.0], + }, + GlyphVertex { local: [1.0, -1.0] }, + GlyphVertex { local: [-1.0, 1.0] }, + GlyphVertex { local: [-1.0, 1.0] }, + GlyphVertex { local: [1.0, -1.0] }, + GlyphVertex { local: [1.0, 1.0] }, + ]; + let glyph_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Pressure Glyph Vertex Buffer"), + contents: bytemuck::cast_slice(&glyph_vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + + let glyph_instances = build_glyph_instances(rows, cols, &layout, 0.0); + let glyph_instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Pressure Glyph Instance Buffer"), + contents: bytemuck::cast_slice(&glyph_instances), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + + Self { + layout, + rows, + cols, + uniform, + uniform_buffer, + uniform_bind_group, + background_pipeline, + glyph_pipeline, + glyph_vertex_buffer, + glyph_instance_buffer, + glyph_instances, + } + } + + fn prepare(&mut self, queue: &wgpu::Queue, width: f32, height: f32, time: f32) { + let aspect = width / height.max(1.0); + self.uniform = + MatrixUniform::new(width, height, build_view_projection(aspect, &self.layout)); + queue.write_buffer( + &self.uniform_buffer, + 0, + bytemuck::cast_slice(&[self.uniform]), + ); + + self.glyph_instances = build_glyph_instances(self.rows, self.cols, &self.layout, time); + queue.write_buffer( + &self.glyph_instance_buffer, + 0, + bytemuck::cast_slice(&self.glyph_instances), + ); + } + + fn paint(&self, render_pass: &mut wgpu::RenderPass<'_>) { + render_pass.set_bind_group(0, &self.uniform_bind_group, &[]); + + render_pass.set_pipeline(&self.background_pipeline); + render_pass.draw(0..3, 0..1); + + render_pass.set_pipeline(&self.glyph_pipeline); + render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..)); + render_pass.set_vertex_buffer(1, self.glyph_instance_buffer.slice(..)); + render_pass.draw(0..6, 0..self.glyph_instances.len() as u32); + } +} + +fn create_background_pipeline( + device: &wgpu::Device, + target_format: &wgpu::TextureFormat, + shader: &wgpu::ShaderModule, + layout: &wgpu::PipelineLayout, +) -> wgpu::RenderPipeline { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Pressure Matrix Background Pipeline"), + layout: Some(layout), + vertex: wgpu::VertexState { + module: shader, + entry_point: Some("vs_background"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: shader, + entry_point: Some("fs_background"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format: *target_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }) +} + +fn create_glyph_pipeline( + device: &wgpu::Device, + target_format: &wgpu::TextureFormat, + shader: &wgpu::ShaderModule, + layout: &wgpu::PipelineLayout, +) -> wgpu::RenderPipeline { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Pressure Matrix Glyph Pipeline"), + layout: Some(layout), + vertex: wgpu::VertexState { + module: shader, + entry_point: Some("vs_glyph"), + compilation_options: Default::default(), + buffers: &[GlyphVertex::desc(), GlyphInstance::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: shader, + entry_point: Some("fs_glyph"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format: *target_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }) +} + +fn build_glyph_instances( + rows: u32, + cols: u32, + layout: &MatrixLayout, + time: f32, +) -> Vec { + let mut instances = Vec::with_capacity((rows * cols) as usize); + + for row in 0..rows { + for col in 0..cols { + let (world_position, normalized) = + glyph_world_position(row, col, rows, cols, layout, time); + instances.push(GlyphInstance { + world_position, + style: [normalized, 0.0, 0.0, 0.0], + }); + } + } + + instances +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct MatrixUniform { + view_proj: [[f32; 4]; 4], + viewport: [f32; 4], + glyph: [f32; 4], + color: [f32; 4], +} + +impl MatrixUniform { + fn new(width: f32, height: f32, view_proj: [[f32; 4]; 4]) -> Self { + Self { + view_proj, + viewport: [width.max(1.0), height.max(1.0), 0.0, 0.0], + glyph: [16.0, 0.0, 0.0, 0.0], + color: [0.05, 0.92, 0.32, 1.0], + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct GlyphVertex { + local: [f32; 2], +} + +impl GlyphVertex { + fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }], + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct GlyphInstance { + world_position: [f32; 4], + style: [f32; 4], +} + +impl GlyphInstance { + fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 1, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, + shader_location: 2, + format: wgpu::VertexFormat::Float32x4, + }, + ], + } + } +} diff --git a/src/shader.wgsl b/src/shader.wgsl index 4eeda29..44867a0 100755 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -1,52 +1,181 @@ -// 顶点着色器 - -struct Camera { +struct MatrixUniform { view_proj: mat4x4f, -} -@group(1) @binding(0) -var camera: Camera; - -struct VertexInput { - @location(0) position: vec3f, - @location(1) tex_coords: vec2f, -} -struct InstanceInput { - @location(5) model_matrix_0: vec4f, - @location(6) model_matrix_1: vec4f, - @location(7) model_matrix_2: vec4f, - @location(8) model_matrix_3: vec4f, + viewport: vec4f, + glyph: vec4f, + color: vec4f, } -struct VertexOutput { +@group(0) @binding(0) +var u: MatrixUniform; + +struct BackgroundVertexOutput { @builtin(position) clip_position: vec4f, - @location(0) tex_coords: vec2f, +} + +struct GlyphVertexInput { + @location(0) local: vec2f, +} + +struct GlyphInstanceInput { + @location(1) world_position: vec4f, + @location(2) style: vec4f, +} + +struct GlyphVertexOutput { + @builtin(position) clip_position: vec4f, + @location(0) local: vec2f, + @location(1) intensity: f32, +} + +fn saturate(value: f32) -> f32 { + return clamp(value, 0.0, 1.0); +} + +fn capsule_distance(point: vec2f, start: vec2f, end: vec2f) -> f32 { + let segment = end - start; + let h = saturate(dot(point - start, segment) / dot(segment, segment)); + return length(point - start - segment * h); +} + +fn rotate2(point: vec2f, angle: f32) -> vec2f { + let s = sin(angle); + let c = cos(angle); + return vec2f(point.x * c - point.y * s, point.x * s + point.y * c); +} + +fn range_stop_color(index: u32) -> vec3f { + switch index { + case 0u: { + return vec3f(0.263, 0.667, 0.420); + } + case 1u: { + return vec3f(0.094, 0.735, 0.875); + } + case 2u: { + return vec3f(0.298, 0.349, 0.957); + } + case 3u: { + return vec3f(0.812, 0.294, 0.851); + } + case 4u: { + return vec3f(0.957, 0.337, 0.333); + } + default: { + return vec3f(1.000, 0.596, 0.204); + } + } +} + +fn sample_range_color(value: f32) -> vec3f { + let t = saturate(value); + + if (t <= 0.25) { + let local = smoothstep(0.0, 0.25, t); + return mix(range_stop_color(0u), range_stop_color(1u), local); + } + + if (t <= 0.5) { + let local = smoothstep(0.25, 0.5, t); + return mix(range_stop_color(1u), range_stop_color(2u), local); + } + + if (t <= 0.75) { + let local = smoothstep(0.5, 0.75, t); + return mix(range_stop_color(2u), range_stop_color(3u), local); + } + + if (t <= 0.875) { + let local = smoothstep(0.75, 0.875, t); + return mix(range_stop_color(3u), range_stop_color(4u), local); + } + + let local = smoothstep(0.875, 1.0, t); + return mix(range_stop_color(4u), range_stop_color(5u), local); +} + +fn digit_zero_alpha(local: vec2f) -> f32 { + let tilted = rotate2(local, -0.18); + let oval = vec2f(tilted.x / 0.48, tilted.y / 0.75); + let aa = 0.030; + let outer = 1.0 - smoothstep(0.93 - aa, 0.93 + aa, length(oval)); + let inner = 1.0 - smoothstep(0.57 - aa, 0.57 + aa, length(oval)); + let ring = saturate(outer - inner); + let slash = 1.0 - smoothstep( + 0.036, + 0.066, + capsule_distance(tilted, vec2f(-0.16, 0.43), vec2f(0.18, -0.43)), + ); + return max(ring, slash * 0.88); } @vertex -fn vs_main( - model: VertexInput, - instance: InstanceInput, -) -> VertexOutput { - let model_matrix = mat4x4f( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, +fn vs_background(@builtin(vertex_index) vertex_index: u32) -> BackgroundVertexOutput { + let positions = array( + vec2f(-1.0, -3.0), + vec2f(3.0, 1.0), + vec2f(-1.0, 1.0), ); - var out: VertexOutput; - out.tex_coords = model.tex_coords; - out.clip_position = camera.view_proj * model_matrix * vec4f(model.position, 1.0); + + var out: BackgroundVertexOutput; + out.clip_position = vec4f(positions[vertex_index], 0.0, 1.0); return out; } -// 片元着色器 +@fragment +fn fs_background(@builtin(position) frag_coord: vec4f) -> @location(0) vec4f { + let pixel = frag_coord.xy; + let viewport = u.viewport.xy; + let uv = pixel / max(viewport, vec2f(1.0, 1.0)); + var color = mix(vec3f(0.112, 0.126, 0.160), vec3f(0.145, 0.160, 0.198), 1.0 - uv.y); + let vignette = smoothstep(0.18, 0.92, length((uv - vec2f(0.52, 0.48)) * vec2f(viewport.x / viewport.y, 1.0))); + color *= 1.0 - vignette * 0.30; + color += vec3f(0.035, 0.070, 0.090) * (1.0 - smoothstep(0.0, 0.9, abs(uv.y - 0.52))); -@group(0) @binding(0) -var t_diffuse: texture_2d; -@group(0)@binding(1) -var s_diffuse: sampler; + let track_width = clamp(viewport.x * 0.42, 260.0, 560.0); + let track_height = 12.0; + let track_center = vec2f(viewport.x * 0.5, viewport.y - 38.0); + let local = pixel - track_center; + let half_size = vec2f(track_width * 0.5, track_height * 0.5); + let inside_track = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y); + let border = step(abs(local.x), half_size.x + 1.0) * step(abs(local.y), half_size.y + 1.0) - inside_track; + let t = saturate((local.x + half_size.x) / track_width); + + if (inside_track > 0.5) { + let shade = mix(0.72, 1.0, 1.0 - saturate((local.y + half_size.y) / track_height)); + color = mix(color, sample_range_color(t) * shade, 0.96); + } + + color = max(color, vec3f(0.34, 0.41, 0.46) * border); + + let tick_area = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y + 8.0); + if (tick_area > 0.5) { + let ticks = array(0.0, 0.25, 0.5, 0.75, 0.875, 1.0); + for (var index = 0u; index < 6u; index = index + 1u) { + let tick_x = (ticks[index] - 0.5) * track_width; + let tick = step(abs(local.x - tick_x), 1.0) * step(abs(local.y), half_size.y + 7.0); + color = max(color, vec3f(0.78, 0.84, 0.90) * tick); + } + } + + return vec4f(color, 1.0); +} + +@vertex +fn vs_glyph(vertex: GlyphVertexInput, instance: GlyphInstanceInput) -> GlyphVertexOutput { + let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0); + let pixel_size = u.glyph.x * mix(0.92, 1.10, saturate(instance.style.x)); + let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0; + + var out: GlyphVertexOutput; + out.clip_position = vec4f(center.xy + ndc_offset * center.w, center.z, center.w); + out.local = vertex.local; + out.intensity = instance.style.x; + return out; +} @fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4f { - return textureSample(t_diffuse, s_diffuse, in.tex_coords); -} \ No newline at end of file +fn fs_glyph(in: GlyphVertexOutput) -> @location(0) vec4f { + let alpha = digit_zero_alpha(in.local); + let color = sample_range_color(in.intensity) * 1.08; + return vec4f(color, alpha); +} diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..1aa4b05 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,114 @@ +use eframe::egui; + +#[derive(Clone, Copy)] +pub struct AppTheme { + pub bg: egui::Color32, + pub panel: egui::Color32, + pub panel_strong: egui::Color32, + pub panel_deep: egui::Color32, + pub border: egui::Color32, + pub border_soft: egui::Color32, + pub text: egui::Color32, + pub text_dim: egui::Color32, + pub accent: egui::Color32, + pub accent_hot: egui::Color32, + pub radius: u8, +} + +pub const ENGINEERING_DARK: AppTheme = AppTheme { + bg: egui::Color32::from_rgb(27, 34, 44), + panel: egui::Color32::from_rgb(29, 40, 52), + panel_strong: egui::Color32::from_rgb(39, 51, 65), + panel_deep: egui::Color32::from_rgb(15, 21, 29), + border: egui::Color32::from_rgb(70, 87, 104), + border_soft: egui::Color32::from_rgb(48, 63, 78), + text: egui::Color32::from_rgb(224, 231, 238), + text_dim: egui::Color32::from_rgb(151, 166, 184), + accent: egui::Color32::from_rgb(255, 121, 47), + accent_hot: egui::Color32::from_rgb(255, 163, 82), + radius: 3, +}; + +pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) { + let mut visuals = egui::Visuals::dark(); + visuals.override_text_color = Some(theme.text); + visuals.panel_fill = theme.bg; + visuals.window_fill = theme.panel; + visuals.window_stroke = egui::Stroke::new(1.0, theme.border); + visuals.extreme_bg_color = theme.panel_deep; + visuals.faint_bg_color = theme.panel_strong; + visuals.code_bg_color = theme.panel_deep; + visuals.warn_fg_color = theme.accent_hot; + visuals.error_fg_color = egui::Color32::from_rgb(255, 98, 82); + + visuals.widgets.noninteractive.bg_fill = theme.panel_strong; + visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft); + visuals.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, theme.text_dim); + visuals.widgets.inactive.bg_fill = theme.panel_strong; + visuals.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft); + visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, theme.text); + visuals.widgets.hovered.bg_fill = egui::Color32::from_rgb(51, 66, 82); + visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, theme.border); + visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, theme.text); + visuals.widgets.active.bg_fill = theme.accent; + visuals.widgets.active.bg_stroke = egui::Stroke::new(1.0, theme.accent_hot); + visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, egui::Color32::WHITE); + visuals.widgets.open.bg_fill = egui::Color32::from_rgb(45, 58, 72); + visuals.widgets.open.bg_stroke = egui::Stroke::new(1.0, theme.border); + visuals.widgets.open.fg_stroke = egui::Stroke::new(1.0, theme.text); + + visuals.selection.bg_fill = theme.accent; + visuals.selection.stroke = egui::Stroke::new(1.0, egui::Color32::WHITE); + visuals.hyperlink_color = theme.accent_hot; + ctx.set_visuals(visuals); + + let mut style = (*ctx.global_style()).clone(); + style.spacing.item_spacing = egui::vec2(7.0, 5.0); + style.spacing.window_margin = egui::Margin::same(7); + style.visuals.window_corner_radius = egui::CornerRadius::same(theme.radius); + style.visuals.menu_corner_radius = egui::CornerRadius::same(2); + style.visuals.window_shadow = egui::epaint::Shadow { + offset: [0, 12], + blur: 24, + spread: 0, + color: egui::Color32::from_black_alpha(110), + }; + ctx.set_global_style(style); +} + +pub fn panel_frame(ctx: &egui::Context) -> egui::Frame { + let style = ctx.global_style(); + egui::Frame::window(&style) + .fill(ENGINEERING_DARK.panel) + .stroke(egui::Stroke::new(1.0, ENGINEERING_DARK.border)) + .corner_radius(egui::CornerRadius::same(ENGINEERING_DARK.radius)) + .shadow(egui::epaint::Shadow { + offset: [0, 12], + blur: 26, + spread: 0, + color: egui::Color32::from_black_alpha(120), + }) +} + +pub fn group_frame() -> egui::Frame { + egui::Frame::new() + .fill(ENGINEERING_DARK.panel_deep) + .stroke(egui::Stroke::new(1.0, ENGINEERING_DARK.border_soft)) + .corner_radius(egui::CornerRadius::same(2)) + .inner_margin(egui::Margin::symmetric(6, 5)) +} + +pub fn tag_button(label: impl Into) -> egui::Button<'static> { + egui::Button::new(label) + .fill(ENGINEERING_DARK.panel_strong) + .stroke(egui::Stroke::new(1.0, ENGINEERING_DARK.border)) + .corner_radius(egui::CornerRadius::same(2)) +} + +pub fn dim_text() -> egui::Color32 { + ENGINEERING_DARK.text_dim +} + +pub fn accent_text() -> egui::Color32 { + ENGINEERING_DARK.accent_hot +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..949505d --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,116 @@ +use eframe::egui; + +use crate::theme::{accent_text, dim_text, group_frame, panel_frame, tag_button}; + +pub struct FloatingPanelState { + pub visible: bool, + default_pos: egui::Pos2, + tag_pos: egui::Pos2, +} + +impl FloatingPanelState { + pub fn new(default_pos: [f32; 2], tag_pos: [f32; 2]) -> Self { + Self { + visible: true, + default_pos: egui::pos2(default_pos[0], default_pos[1]), + tag_pos: egui::pos2(tag_pos[0], tag_pos[1]), + } + } +} + +pub fn draw_scene_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) { + draw_floating_panel(ctx, panel, "Scene", "scene_panel", |ui| { + ui.horizontal(|ui| { + ui.colored_label(dim_text(), "View"); + let _ = ui.selectable_label(true, "Clusters"); + let _ = ui.selectable_label(false, "Triangles"); + }); + ui.separator(); + group_frame().show(ui, |ui| { + ui.label("Models / materials / lights"); + ui.label("target tasks 64"); + ui.label("slack cache 100.0%"); + }); + }); +} + +pub fn draw_config_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) { + draw_floating_panel(ctx, panel, "Config", "config_panel", |ui| { + ui.horizontal(|ui| { + ui.label("Shader"); + let _ = ui.selectable_label(true, "Clusters"); + let _ = ui.selectable_label(false, "wireframe"); + }); + ui.horizontal(|ui| { + ui.label("Grid"); + ui.label("x 32"); + ui.label("z 32"); + ui.label("side 1.000"); + }); + }); +} + +pub fn draw_stats_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) { + draw_floating_panel(ctx, panel, "Stats", "stats_panel", |ui| { + ui.horizontal(|ui| { + ui.colored_label(accent_text(), "0.030"); + ui.label("81m:51s"); + }); + ui.separator(); + group_frame().show(ui, |ui| { + ui.label("FPS / GPU info"); + ui.label("bounds 589.0us"); + ui.label("clusters 12.8ms"); + }); + }); +} + +fn draw_floating_panel( + ctx: &egui::Context, + panel: &mut FloatingPanelState, + title: &'static str, + id: &'static str, + add_contents: impl FnOnce(&mut egui::Ui), +) { + if panel.visible { + let mut open = true; + let mut hide_requested = false; + + egui::Window::new(title) + .id(egui::Id::new(id)) + .open(&mut open) + .default_pos(panel.default_pos) + .title_bar(false) + .resizable(true) + .frame(panel_frame(ctx)) + .show(ctx, |ui| { + ui.horizontal(|ui| { + if ui.add(tag_button("Hide")).clicked() { + hide_requested = true; + } + ui.add_space(6.0); + ui.colored_label(dim_text(), title); + }); + ui.separator(); + add_contents(ui); + }); + + panel.visible = open && !hide_requested; + } else { + let response = egui::Area::new(egui::Id::new(format!("{id}_tag"))) + .current_pos(panel.tag_pos) + .movable(true) + .order(egui::Order::Foreground) + .show(ctx, |ui| { + ui.set_min_width(86.0); + if ui + .add(tag_button(format!("▸ {title}")).min_size(egui::vec2(86.0, 22.0))) + .clicked() + { + panel.visible = true; + } + }); + + panel.tag_pos = response.response.rect.min; + } +}