All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
1092 lines
45 KiB
Rust
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);
|
|
}
|
|
}
|