use std as alloc; use std::borrow::Cow; use std::ops::DerefMut; use std::sync::Arc; use cosmic_jotdown::jotdown::{self, Event, ListKind}; use cosmic_jotdown::{Indent, INDENT_AMOUNT}; use eframe::egui::mutex::{Mutex, RwLock}; use eframe::egui::{ self, lerp, Align2, Image, ImageSize, ImageSource, OpenUrl, Pos2, Rect, Rounding, Sense, Stroke, Vec2, }; use eframe::egui_wgpu::{self, wgpu}; use egui::{Color32, Frame}; use egui_glyphon::glyphon::cosmic_text::{Align, BufferRef}; use egui_glyphon::glyphon::{self, Attrs, Color, Edit, Editor, Weight}; use egui_glyphon::{BufferWithTextArea, GlyphonRenderer, GlyphonRendererCallback}; use encase::ShaderType; use glam::Mat2; use glyphon::{Buffer, FontSystem, Metrics}; use keyframe::functions; use range_map::RangeMap; use wgpu::util::DeviceExt; /// We derive Deserialize/Serialize so we can persist app state on shutdown. pub struct Portfolio { custom: Custom3d, zoomed: bool, hovered: bool, click_time_offset: f64, hover_time_offset: f64, from_size: f32, to_size: f32, from_pos: [f32; 2], to_pos: [f32; 2], from_beamwidth: f32, to_beamwidth: f32, font_system: Arc>, name_buffers: [(Arc>, Vec2); 3], window: ContextWindow, buffer_size: Vec2, max_size: Rect, } #[derive(Default)] pub struct ContextWindow { pub size: Vec2, pub text: Vec<(Rect, Indent, ContextBlock)>, } pub struct ContextIcon { pub icon: ImageSource<'static>, pub content: &'static Cow<'static, [Event<'static>]>, pub angle_at: f32, pub name: &'static str, } #[derive(Clone)] pub enum ContextBlock { Buffer(Arc>, Option>), Image { alt_text: Arc>, image: Image<'static>, }, } const CONTEXT_METRICS: Metrics = Metrics::new(16.0, 18.0); const ABOUT_ME: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/about_me.jot")); const INKSCAPE: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/inkscape.jot")); const RUST: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/rust.jot")); const GIMP: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/gimp.jot")); const REAPER: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/reaper.jot")); const TREE_SITTER: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/tree_sitter.jot")); const LINUX: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/linux.jot")); const PYTHON: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/python.jot")); const KDENLIVE: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/kdenlive.jot")); const JAVA: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/java.jot")); const SCRATCH: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/scratch.jot")); const POSTGRESQL: Cow<'static, [Event<'static>]> = include!(concat!(env!("OUT_DIR"), "/postgresql.jot")); const CONTEXT_ICONS: [ContextIcon; 11] = [ ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!( env!("PHOST"), "/images/inkscape.png" ))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/inkscape.png")), content: &INKSCAPE, angle_at: -142.0, name: "inkscape", }, ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!(env!("PHOST"), "/images/rust.png"))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/rust.png")), content: &RUST, angle_at: -66.0, name: "rust", }, ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!(env!("PHOST"), "/images/gimp.png"))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/gimp.png")), content: &GIMP, angle_at: -223.0, name: "gimp", }, ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!(env!("PHOST"), "/images/reaper.png"))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/reaper.png")), content: &REAPER, angle_at: -90.0, name: "reaper", }, ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!( env!("PHOST"), "/images/tree_sitter.png" ))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/tree_sitter.png")), content: &TREE_SITTER, angle_at: 37.0, name: "tree_sitter", }, ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!(env!("PHOST"), "/images/linux.png"))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/linux.png")), content: &LINUX, angle_at: -270.0, name: "linux", }, ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!(env!("PHOST"), "/images/python.png"))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/python.png")), content: &PYTHON, angle_at: -40.0, name: "python", }, ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!( env!("PHOST"), "/images/kdenlive.png" ))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/kdenlive.png")), content: &KDENLIVE, angle_at: -205.0, name: "kdenlive", }, ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!(env!("PHOST"), "/images/java.png"))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/java.png")), content: &JAVA, angle_at: -105.0, name: "java", }, ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!(env!("PHOST"), "/images/scratch.png"))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/scratch.png")), content: &SCRATCH, angle_at: -52.0, name: "scratch", }, ContextIcon { #[cfg(target_arch = "wasm32")] icon: ImageSource::Uri(Cow::Borrowed(concat!( env!("PHOST"), "/images/postgresql.png" ))), #[cfg(not(target_arch = "wasm32"))] icon: ImageSource::Uri(Cow::Borrowed("file://assets/images/postgresql.png")), content: &POSTGRESQL, angle_at: -235.0, name: "postgresql", }, ]; const NAME_PLATE: [&str; 3] = [ "Isaac Mills", "Computer Scientist; Software Engineer", "My portfolio", ]; impl ContextWindow { pub fn set_content( &mut self, content: &Cow<'static, [Event<'static>]>, font_system: &mut FontSystem, mut max_width: f32, ) { self.size = Vec2::new(max_width / 1.5, 0.0); let mut last_indent = None; let mut last_image_size: Option = None; max_width /= 1.5; self.text = cosmic_jotdown::jotdown_into_buffers( content.iter().cloned(), font_system, CONTEXT_METRICS, max_width, ) .map(|buffer| { let measurement = measure_buffer(&buffer.buffer, self.size); let paragraph_height = if last_indent.is_none() || buffer.indent.modifier.is_none() { CONTEXT_METRICS.line_height * 1.5 } else { 8.0 }; last_indent = buffer.indent.modifier; let buffer = if let Some(url) = buffer.image_url { let image; let url = url.split_once('#').unwrap(); let size = url.1.split_once('x').unwrap(); let size = Vec2::new(size.0.parse().unwrap(), size.1.parse().unwrap()); #[cfg(target_arch = "wasm32")] { image = Image::from_uri(format!(concat!(env!("PHOST"), "/{}"), url.0)); } #[cfg(not(target_arch = "wasm32"))] { image = Image::from_uri(format!("file://assets/{}", url.0)); } let mut res = ( Rect::from_min_size( Pos2::new(buffer.indent.indent, self.size.y + paragraph_height), size, ), buffer.indent, ContextBlock::Image { alt_text: Arc::new(RwLock::new(buffer.buffer)), image, }, ); const IMAGE_PADDING: f32 = 8.0; if let Some(last_size) = last_image_size.as_mut() { let ls = *last_size; last_size.x += size.x + IMAGE_PADDING; if last_size.x > max_width { self.size.y += last_size.y + paragraph_height; last_size.x = size.x + IMAGE_PADDING; last_size.y = size.y; res.0 = Rect::from_min_size( Pos2::new(buffer.indent.indent, self.size.y + paragraph_height), size, ); } else { last_size.y = last_size.y.max(size.y); res.0 = res.0.translate(Vec2::new(ls.x, 0.0)); } } else { if size.x > max_width { let max_size = Vec2::new(max_width, size.y); let new_size = ImageSize { max_size, ..Default::default() } .calc_size(max_size, size); res.0 = Rect::from_min_size( Pos2::new(buffer.indent.indent, self.size.y + paragraph_height), new_size, ); self.size.y += new_size.y + paragraph_height; } else { last_image_size = Some(size); } } res } else { if let Some(size) = last_image_size { self.size.y += size.y + paragraph_height; } let res = ( Rect::from_min_size( Pos2::new(buffer.indent.indent, self.size.y + paragraph_height), measurement.size(), ), buffer.indent, ContextBlock::Buffer(Arc::new(RwLock::new(buffer.buffer)), buffer.url_map), ); last_image_size = None; self.size.y += measurement.height() + paragraph_height; res }; self.size.x = self.size.x.max(measurement.width()); buffer }) .collect(); if let Some(size) = last_image_size { self.size.y += size.y; } } } fn make_buffers( font_system: &mut FontSystem, scale: f32, ) -> (Vec2, [(Arc>, Vec2); 3]) { let mut buffers = [ ( Buffer::new(font_system, Metrics::new(64.0 * scale, 70.0 * scale)), Vec2::ZERO, Weight::NORMAL, ), ( Buffer::new(font_system, Metrics::new(24.0 * scale, 30.0 * scale)), Vec2::ZERO, Weight::LIGHT, ), ( Buffer::new(font_system, Metrics::new(24.0 * scale, 30.0 * scale)), Vec2::ZERO, Weight::LIGHT, ), ]; let mut buffer_size = Vec2::ZERO; for buffer in buffers.iter_mut().zip(NAME_PLATE.into_iter()) { buffer.0 .0.set_size(font_system, f32::MAX, f32::MAX); buffer.0 .0.set_text( font_system, buffer.1, Attrs::new() .family(glyphon::Family::SansSerif) .weight(buffer.0 .2), glyphon::Shaping::Basic, ); buffer.0 .0.shape_until_scroll(font_system, true); let size = measure_buffer(&buffer.0 .0, Vec2::INFINITY).size(); buffer_size.y += size.y; buffer_size.x = buffer_size.x.max(size.x); buffer.0 .1 = size; } ( buffer_size, buffers.map(|b| (Arc::new(RwLock::new(b.0)), b.1)), ) } impl Portfolio { /// Called once before the first frame. pub fn new(cc: &eframe::CreationContext<'_>) -> Self { // This is also where you can customize the look and feel of egui using // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`. // Load previous app state (if any). // Note that you must enable the `persistence` feature for this to work. egui_extras::install_image_loaders(&cc.egui_ctx); let mut font_system = FontSystem::new(); font_system .db_mut() .load_font_source(glyphon::fontdb::Source::Binary(Arc::new(include_bytes!( "/usr/share/fonts/ubuntu/Ubuntu-L.ttf" )))); font_system .db_mut() .load_font_source(glyphon::fontdb::Source::Binary(Arc::new(include_bytes!( "/usr/share/fonts/ubuntu/Ubuntu-R.ttf" )))); font_system .db_mut() .load_font_source(glyphon::fontdb::Source::Binary(Arc::new(include_bytes!( "/usr/share/fonts/ubuntu/Ubuntu-RI.ttf" )))); font_system .db_mut() .load_font_source(glyphon::fontdb::Source::Binary(Arc::new(include_bytes!( "/usr/share/fonts/ubuntu/Ubuntu-B.ttf" )))); font_system .db_mut() .load_font_source(glyphon::fontdb::Source::Binary(Arc::new(include_bytes!( "/usr/share/fonts/TTF/FiraCode-Regular.ttf" )))); let (buffer_size, name_buffers) = make_buffers(&mut font_system, 1.0); let font_system = Arc::new(Mutex::new(font_system)); GlyphonRenderer::insert( cc.wgpu_render_state.as_ref().unwrap(), Arc::clone(&font_system), ); Self { custom: Custom3d::new(cc).unwrap(), zoomed: false, hovered: false, click_time_offset: 0.0, hover_time_offset: 0.0, from_size: 0.6, to_size: 0.6, from_pos: [0.0, 0.0], to_pos: [0.0, 0.0], from_beamwidth: 2.0, to_beamwidth: 2.0, font_system, name_buffers, buffer_size, window: ContextWindow::default(), max_size: Rect::ZERO, } } pub fn click(&mut self, ui: &egui::Ui, transform: Option) { self.from_size = self.to_size; self.from_pos = self.to_pos; self.click_time_offset = ui.input(|i| i.time); if self.zoomed { self.to_size = 0.6; self.to_pos = [0.0, 0.0]; self.zoomed = false; } else { self.to_size = 0.1; if let Some(transform) = transform { let point = transform.mul_vec2(glam::Vec2::new(2150.0, 0.0)); self.to_pos = point.into(); } self.zoomed = true; log::info!("{:?}", (self.from_size, self.from_pos)); } } pub fn hover(&mut self, hovered: bool, ui: &egui::Ui) { if self.hovered != hovered { self.hover_time_offset = ui.input(|i| i.time); } self.hovered = hovered; if hovered { self.to_beamwidth = 6.0; self.from_beamwidth = 2.0; } else { self.to_beamwidth = 2.0; self.from_beamwidth = 6.0; } } } pub fn measure_buffer(buffer: &Buffer, vb: Vec2) -> Rect { let mut rtl = false; let (width, total_lines) = buffer .layout_runs() .fold((0.0, 0usize), |(width, total_lines), run| { if run.rtl { rtl = true; } (run.line_w.max(width), total_lines + 1) }); let (max_width, max_height) = buffer.size(); let size = Vec2::new( if rtl { vb.x } else { width.min(max_width) }, (total_lines as f32 * buffer.metrics().line_height).min(max_height), ); match buffer.lines[0].align() { Some(Align::Right) | Some(Align::End) => { Align2::RIGHT_TOP.align_size_within_rect(size, Rect::from_min_size(Pos2::ZERO, vb)) } Some(Align::Center) | Some(Align::Justified) => { Align2::CENTER_TOP.align_size_within_rect(size, Rect::from_min_size(Pos2::ZERO, vb)) } Some(Align::Left) | None => Rect::from_min_size(Pos2::ZERO, size), } } impl Portfolio { fn update_customs(&mut self, ui: &mut egui::Ui) { let ppi = ui.ctx().pixels_per_point(); self.custom.resolution = glam::Vec2::new(ui.max_rect().width() * ppi, ui.max_rect().height() * ppi); self.custom.time = ui.input(|i| i.time as f32); if let Some(pos) = ui.input(|i| i.pointer.latest_pos()) { let pos = pos * ppi; self.custom.cursor = glam::Vec2::new(pos.x, self.custom.resolution.y - pos.y); } self.custom.size = keyframe::ease_with_scaled_time( functions::EaseOutCubic, self.from_size, self.to_size, ui.input(|i| i.time) - self.click_time_offset, 1.0, ); self.custom.offset = glam::Vec2::from_array(keyframe::ease_with_scaled_time( functions::EaseOutCubic, self.from_pos, self.to_pos, ui.input(|i| i.time) - self.click_time_offset, 1.0, )); self.custom.beam_width = keyframe::ease_with_scaled_time( functions::EaseOutCubic, self.from_beamwidth, self.to_beamwidth, ui.input(|i| i.time) - self.hover_time_offset, 0.5, ); } } impl eframe::App for Portfolio { /// Called each time the UI needs repainting, which may be many times per second. fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { // Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. // For inspiration and more examples, go to https://emilk.github.io/egui egui::CentralPanel::default() .frame(Frame::default().fill(Color32::BLACK)) .show(ctx, |ui| { self.update_customs(ui); self.custom.custom_painting(ui); if ui.max_rect() != self.max_size { let scale = Vec2::from(self.custom.resolution.to_array()) / ui.ctx().pixels_per_point() / Vec2::new(800.0, 800.0); let (buffer_size, name_buffers) = make_buffers(self.font_system.lock().deref_mut(), scale.x.min(scale.y)); self.buffer_size = buffer_size; self.name_buffers = name_buffers; } self.max_size = ui.max_rect(); let mut scroll_area = egui::ScrollArea::vertical().show_viewport(ui, |ui, rect| { let max_rect = ui.max_rect(); ui.allocate_exact_size( Vec2::new( max_rect.width(), self.window.size.y + CONTEXT_METRICS.line_height * 8.0, ), Sense::hover(), ); rect.translate(Vec2::new(0.0, (-rect.min.y) * 2.0)) }); let name_rect = Align2::CENTER_CENTER.align_size_within_rect(self.buffer_size, ui.max_rect()); let name_opacity = keyframe::ease_with_scaled_time( functions::EaseOutCubic, if self.zoomed { 1.0 } else { 0.0 }, if self.zoomed { 0.0 } else { 1.0 }, ui.input(|i| i.time) - self.click_time_offset - if self.zoomed { 0.0 } else { 0.5 }, 1.0, ); let zoom_view_opacity = keyframe::ease_with_scaled_time( functions::EaseOutCubic, if self.zoomed { 0.0 } else { 1.0 }, if self.zoomed { 1.0 } else { 0.0 }, ui.input(|i| i.time) - self.click_time_offset, 1.0, ); let mut buffer_offset = 0.0; let mut buffers: Vec = self .name_buffers .iter() .map(|b| { let res = BufferWithTextArea::new( b.0.clone(), name_rect.translate(Vec2::new(0.0, buffer_offset)), name_opacity, egui_glyphon::glyphon::Color::rgb(255, 255, 255), ui.ctx(), ); buffer_offset += b.1.y; res }) .collect(); let name_resp = ui.allocate_rect(name_rect, Sense::click()); let center = Pos2::new(ui.max_rect().width() / 2.0, ui.max_rect().height() / 2.0); let reference_rect = if ui.max_rect().height() > ui.max_rect().width() { let size = Vec2::new(ui.max_rect().width(), ui.max_rect().width()) * Vec2::new(0.85, 0.85); Align2::CENTER_CENTER.align_size_within_rect(size, ui.max_rect()) } else { let size = Vec2::new(ui.max_rect().height(), ui.max_rect().height()) * Vec2::new(0.85, 0.85); Align2::CENTER_CENTER.align_size_within_rect(size, ui.max_rect()) }; let mut icon_link = None; let rect = if ui.max_rect().height() > self.window.size.y { Align2::CENTER_CENTER .align_size_within_rect(self.window.size, scroll_area.inner) } else { Align2::CENTER_TOP .align_size_within_rect(self.window.size, scroll_area.inner) .translate(Vec2::new(0.0, CONTEXT_METRICS.line_height * 4.0)) }; let mut link_clicked = false; if zoom_view_opacity > 0.0 { ui.painter().rect_filled( Rect::from_min_size(rect.min, self.window.size).expand(25.0), Rounding::same(25.0), Color32::BLACK.gamma_multiply(lerp(0.0..=0.5, zoom_view_opacity)), ); self.window .text .iter() .for_each(|context_block| match &context_block.2 { ContextBlock::Buffer(buffer, url_map) => { let text_rect = context_block.0.translate(rect.min.to_vec2()); // ui.painter().debug_rect(text_rect, Color32::GREEN, ""); if let Some(url_map) = url_map { let text_response = ui.allocate_rect(text_rect, Sense::click()); if text_response.clicked() { let mut buffer = buffer.write(); let mut editor = Editor::new(BufferRef::Borrowed(buffer.deref_mut())); let mouse_click = ui.input(|i| { i.pointer.interact_pos().unwrap_or_default() }) - text_rect.min.to_vec2(); editor.action( self.font_system.lock().deref_mut(), glyphon::Action::Click { x: mouse_click.x as i32, y: mouse_click.y as i32, }, ); let mut location = editor.cursor(); match location.affinity { glyphon::Affinity::After => location.index += 1, glyphon::Affinity::Before => {} } if let Some(url) = url_map.get(location.index) { link_clicked = true; if url.starts_with('#') { if let Some(icon) = CONTEXT_ICONS .iter() .find(|i| i.name == &url[1..]) { icon_link = Some(icon); } } else { ui.ctx().open_url(OpenUrl::new_tab(url)); } // clicked = false; } } } buffers.push(BufferWithTextArea::new( buffer.clone(), text_rect, zoom_view_opacity, Color::rgb(255, 255, 255), ui.ctx(), )); match context_block.1.modifier { Some(ListKind::Unordered) => { ui.painter().circle( text_rect.min + Vec2::new( -INDENT_AMOUNT, CONTEXT_METRICS.line_height / 2.0, ), 2.5, Color32::WHITE.gamma_multiply(zoom_view_opacity), Stroke::NONE, ); } _ => {} } } ContextBlock::Image { image, .. } => { image .clone() .tint(Color32::WHITE.gamma_multiply(zoom_view_opacity)) .paint_at(ui, context_block.0.translate(rect.min.to_vec2())); } }); } let mut hovered = false; if self.zoomed { if ui.input(|i| i.pointer.any_click()) && icon_link.is_none() && !link_clicked { self.click(ui, None); } } else { if ui.input(|i| i.pointer.secondary_clicked()) { let mouse = ui.input(|i| i.pointer.interact_pos().unwrap_or_default()); let center = glam::Vec2::new( ui.max_rect().width() / 2.0, ui.max_rect().height() / 2.0, ); let angle = ((mouse.x - center.x).atan2(mouse.y - center.y) * 180.0 / std::f32::consts::PI) - 90.0; log::info!("{}", angle); } if name_resp.clicked() { self.click(ui, None); self.window.set_content( &ABOUT_ME, self.font_system.lock().deref_mut(), ui.max_rect().width(), ); } hovered = name_resp.hovered() || hovered; for i in CONTEXT_ICONS.iter() { let transform = glam::Mat2::from_angle((-i.angle_at).to_radians()); let factor = reference_rect.size() / 775.0; let point = center + Vec2::from( transform .mul_vec2(glam::Vec2::new(reference_rect.width() / 2.0, 0.0)) .to_array(), ) - (Vec2::splat(32.0) * factor); let rect = Rect::from_min_size(point, Vec2::new(64.0, 64.0) * factor); let response = ui.allocate_rect(rect, Sense::click()); let response_hovered = response.hovered(); let time = ui.input(|i| i.time); let opacity = ui.memory_mut(|m| { let icon_state = m.data.get_temp_mut_or_insert_with(response.id, || { (0.0, response_hovered) }); if response_hovered != icon_state.1 { icon_state.0 = time; icon_state.1 = response_hovered; } lerp( 0.0..=keyframe::ease_with_scaled_time( functions::EaseOutCubic, if response_hovered { 0.5 } else { 1.0 }, if response_hovered { 1.0 } else { 0.5 }, time - icon_state.0, 0.5, ), name_opacity, ) }); if response.clicked() { self.window.set_content( i.content, self.font_system.lock().deref_mut(), ui.max_rect().width(), ); self.click(ui, Some(transform)); scroll_area.state.offset = Vec2::ZERO; scroll_area.state.store(ui.ctx(), scroll_area.id); } hovered = response_hovered || hovered; egui::Image::new(i.icon.clone()) .tint(Color32::WHITE.gamma_multiply(opacity)) .paint_at(ui, rect); } } if let Some(icon) = icon_link { self.window.set_content( icon.content, self.font_system.lock().deref_mut(), ui.max_rect().width(), ); self.zoomed = false; self.click( ui, Some(glam::Mat2::from_angle((-icon.angle_at).to_radians())), ); self.zoomed = true; scroll_area.state.offset = Vec2::ZERO; scroll_area.state.store(ui.ctx(), scroll_area.id); } self.hover(hovered, ui); ui.painter().add(egui_wgpu::Callback::new_paint_callback( ui.max_rect(), GlyphonRendererCallback { buffers }, )); }); ctx.request_repaint(); } } #[derive(Copy, Clone, Default, ShaderType, Debug)] #[allow(unused_attributes)] #[repr(C)] pub struct Custom3d { pub resolution: glam::Vec2, pub time: f32, pub cursor: glam::Vec2, pub size: f32, pub offset: glam::Vec2, pub beam_width: f32, paddingtwo: f32, } impl Custom3d { pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Option { // Get the WGPU render state from the eframe creation context. This can also be retrieved // from `eframe::Frame` when you don't have a `CreationContext` available. let wgpu_render_state = cc.wgpu_render_state.as_ref()?; let device = &wgpu_render_state.device; let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("3d"), source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), }); let constants = Self::default(); let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Uniform Buffer"), contents: { let mut buffer = encase::UniformBuffer::new(Vec::new()); buffer.write(&constants).unwrap(); buffer.into_inner().as_slice() }, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }); let uniform_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::all(), ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }], label: Some("uniform_bind_group_layout"), }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &uniform_bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform_buffer.as_entire_binding(), }], label: Some("uniform_bind_group"), }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &[&uniform_bind_group_layout], push_constant_ranges: &[], }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: None, layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: "vs_main", buffers: &[], }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_main", targets: &[Some(wgpu::ColorTargetState { format: wgpu_render_state.target_format, blend: Some(wgpu::BlendState { color: wgpu::BlendComponent::REPLACE, alpha: wgpu::BlendComponent::REPLACE, }), write_mask: wgpu::ColorWrites::ALL, })], }), primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview: None, }); // Because the graphics pipeline must have the same lifetime as the egui render pass, // instead of storing the pipeline in our `Custom3D` struct, we insert it into the // `paint_callback_resources` type map, which is stored alongside the render pass. wgpu_render_state .renderer .write() .callback_resources .insert(TriangleRenderResources { pipeline, bind_group, uniform_buffer, }); Some(constants) } } // Callbacks in egui_wgpu have 3 stages: // * prepare (per callback impl) // * finish_prepare (once) // * paint (per callback impl) // // The prepare callback is called every frame before paint and is given access to the wgpu // Device and Queue, which can be used, for instance, to update buffers and uniforms before // rendering. // If [`egui_wgpu::Renderer`] has [`egui_wgpu::FinishPrepareCallback`] registered, // it will be called after all `prepare` callbacks have been called. // You can use this to update any shared resources that need to be updated once per frame // after all callbacks have been processed. // // On both prepare methods you can use the main `CommandEncoder` that is passed-in, // return an arbitrary number of user-defined `CommandBuffer`s, or both. // The main command buffer, as well as all user-defined ones, will be submitted together // to the GPU in a single call. // // The paint callback is called after finish prepare and is given access to egui's main render pass, // which can be used to issue draw commands. struct CustomTriangleCallback { constants: Custom3d, } impl egui_wgpu::CallbackTrait for CustomTriangleCallback { 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: &TriangleRenderResources = resources.get().unwrap(); resources.prepare(device, queue, &self.constants); Vec::new() } fn paint<'a>( &self, _info: egui::PaintCallbackInfo, render_pass: &mut wgpu::RenderPass<'a>, resources: &'a egui_wgpu::CallbackResources, ) { let resources: &TriangleRenderResources = resources.get().unwrap(); resources.paint(render_pass); } } impl Custom3d { fn custom_painting(&mut self, ui: &mut egui::Ui) { ui.painter().add(egui_wgpu::Callback::new_paint_callback( ui.max_rect(), CustomTriangleCallback { constants: *self }, )); } } struct TriangleRenderResources { pipeline: wgpu::RenderPipeline, bind_group: wgpu::BindGroup, uniform_buffer: wgpu::Buffer, } impl TriangleRenderResources { fn prepare(&self, _device: &wgpu::Device, queue: &wgpu::Queue, constants: &Custom3d) { // Update our uniform buffer with the angle from the UI let mut buffer = encase::UniformBuffer::new(Vec::new()); buffer.write(constants).unwrap(); queue.write_buffer(&self.uniform_buffer, 0, buffer.into_inner().as_slice()); } fn paint<'rp>(&'rp self, render_pass: &mut wgpu::RenderPass<'rp>) { // Draw our triangle! render_pass.set_pipeline(&self.pipeline); render_pass.set_bind_group(0, &self.bind_group, &[]); render_pass.draw(0..3, 0..1); } }