portfolio/src/app.rs
Isaac Mills d464039cc5
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Fix image zooming
2024-03-18 23:43:06 -04:00

1092 lines
45 KiB
Rust

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, Id, Image, ImageSize, ImageSource, LayerId, 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,
image_zoomed: bool,
last_image_zoomed: Id,
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<Mutex<FontSystem>>,
name_buffers: [(Arc<RwLock<Buffer>>, 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<RwLock<Buffer>>, Option<RangeMap<usize, &'static str>>),
Image {
alt_text: Arc<RwLock<Buffer>>,
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<Vec2> = 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 + Vec2::new(IMAGE_PADDING, 0.0));
}
}
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<RwLock<Buffer>>, 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,
image_zoomed: false,
last_image_zoomed: Id::NULL,
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<Mat2>) {
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;
}
}
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 time = ui.input(|i| i.time);
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 },
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 },
time - self.click_time_offset,
1.0,
);
let mut buffer_offset = 0.0;
let mut buffers: Vec<BufferWithTextArea> = 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, .. } => {
let image_rect = context_block.0.translate(rect.min.to_vec2());
let image_response = ui.allocate_rect(image_rect, Sense::click());
let time_offset = ui.memory_mut(|m| {
let time_offset =
m.data.get_temp_mut_or_default::<f64>(image_response.id);
if image_response.clicked() && !self.image_zoomed && self.zoomed
{
link_clicked = true;
*time_offset = time;
self.last_image_zoomed = image_response.id;
self.image_zoomed = true;
}
*time_offset
});
if image_response.hovered() && !self.image_zoomed {
ui.ctx().set_cursor_icon(egui::CursorIcon::ZoomIn);
}
let fs_rect = Align2::CENTER_CENTER.align_size_within_rect(
{
let max_size = self.max_size.shrink(16.0).size();
ImageSize {
max_size,
..Default::default()
}
.calc_size(max_size, image_rect.size())
},
self.max_size,
);
if self.last_image_zoomed == image_response.id {
let ui = egui::Ui::new(
ui.ctx().clone(),
LayerId::debug(),
Id::new("image_zoom"),
self.max_size,
self.max_size,
);
let t = keyframe::ease_with_scaled_time(
functions::EaseInOutCubic,
if self.image_zoomed { 0.0 } else { 1.0 },
if self.image_zoomed { 1.0 } else { 0.0 },
time - time_offset,
0.5,
);
ui.painter().rect_filled(
ui.max_rect(),
Rounding::default(),
Color32::BLACK.gamma_multiply(
keyframe::ease_with_scaled_time(
functions::EaseInOutCubic,
if self.image_zoomed { 0.0 } else { 0.6 },
if self.image_zoomed { 0.6 } else { 0.0 },
time - time_offset,
0.5,
),
),
);
image
.clone()
.tint(Color32::WHITE.gamma_multiply(zoom_view_opacity))
.paint_at(&ui, image_rect.lerp_towards(&fs_rect, t));
} else {
image
.clone()
.tint(Color32::WHITE.gamma_multiply(zoom_view_opacity))
.paint_at(
ui,
image_rect.lerp_towards(
&fs_rect,
keyframe::ease_with_scaled_time(
functions::EaseInOutCubic,
1.0,
0.0,
time - time_offset,
0.5,
),
),
);
}
}
});
}
let mut hovered = false;
if self.zoomed {
if ui.input(|i| i.pointer.any_click()) && !link_clicked {
if self.image_zoomed {
ui.memory_mut(|m| {
let time_offset = m
.data
.get_temp_mut_or_default::<f64>(self.last_image_zoomed);
link_clicked = true;
*time_offset = time;
self.image_zoomed = false;
});
} else if icon_link.is_none() {
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 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<Self> {
// 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<wgpu::CommandBuffer> {
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);
}
}