This commit is contained in:
parent
11bbb2500f
commit
a7f3c12018
5 changed files with 460 additions and 498 deletions
482
Cargo.lock
generated
482
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
@ -7,8 +7,10 @@ rust-version = "1.72"
|
|||
|
||||
|
||||
[dependencies]
|
||||
cosmic-jotdown = { git = "https://git.nations.lol/fnmain/cosmic-jotdown" }
|
||||
eframe = { version = "0.26.2", default-features = false, features = [
|
||||
# cosmic-jotdown = { git = "https://git.nations.lol/fnmain/cosmic-jotdown" }
|
||||
cosmic-jotdown = {path = "../cosmic-jotdown"}
|
||||
jotdown = { git = "https://git.nations.lol/fnmain/jotdown" }
|
||||
eframe = { version = "0.27.2", default-features = false, features = [
|
||||
"accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies.
|
||||
# "default_fonts",
|
||||
"wgpu", # Use the glow rendering backend. Alternative: "wgpu".
|
||||
|
@ -16,17 +18,17 @@ eframe = { version = "0.26.2", default-features = false, features = [
|
|||
"wayland",
|
||||
] }
|
||||
egui-glyphon = { git = "https://git.nations.lol/fnmain/egui-glyphon" }
|
||||
egui_extras = { version = "0.26.2", features = ["image", "http", "file"] }
|
||||
egui_extras = { version = "0.27.2", features = ["image", "http", "file"] }
|
||||
encase = { version = "0.7.0", features = ["glam"] }
|
||||
glam = "0.25.0"
|
||||
image = { version = "0.24.9", features = ["jpeg", "png"] }
|
||||
image = { version = "0.24.9", features = ["jpeg", "png"], defaut-features = false }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
log = "0.4"
|
||||
range-map = "0.2.0"
|
||||
rangemap = "1.5.1"
|
||||
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
env_logger = "0.10"
|
||||
env_logger = "0.11"
|
||||
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.7 MiB |
|
@ -12,4 +12,4 @@
|
|||
- I balance the time I spend at my computer with outdoor activities like, camping, hiking, and backpacking
|
||||
- I know how to both write code for computers, as well as whip them into shape when they start having problems. I am the IT guy in my family
|
||||
|
||||
\*Special thanks to _meico_ from Shadertoy for the GLSL version of your Spectrum Ring shader
|
||||
\*Special thanks to _meico_ from Shadertoy for the GLSL version of your [Spectrum Ring shader](https://www.shadertoy.com/view/XssXWH)
|
||||
|
|
344
src/app.rs
344
src/app.rs
|
@ -13,12 +13,11 @@ use std::sync::Arc;
|
|||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use arc_swap::ArcSwapAny;
|
||||
use cosmic_jotdown::jotdown::{self, Event, ListKind};
|
||||
use cosmic_jotdown::{Indent, INDENT_AMOUNT};
|
||||
use cosmic_jotdown::Indent;
|
||||
use eframe::egui::mutex::{Mutex, RwLock};
|
||||
use eframe::egui::{
|
||||
self, lerp, Align2, Id, Image, ImageSize, ImageSource, LayerId, OpenUrl, Pos2, Rect, Rounding,
|
||||
Sense, Stroke, Vec2,
|
||||
Sense, Vec2,
|
||||
};
|
||||
use eframe::egui_wgpu::{self, wgpu};
|
||||
use egui::{Color32, Frame};
|
||||
|
@ -28,8 +27,9 @@ use egui_glyphon::{BufferWithTextArea, GlyphonRenderer, GlyphonRendererCallback}
|
|||
use encase::ShaderType;
|
||||
use glam::Mat2;
|
||||
use glyphon::{Buffer, FontSystem, Metrics};
|
||||
use jotdown::Event;
|
||||
use keyframe::functions;
|
||||
use range_map::RangeMap;
|
||||
use rangemap::RangeMap;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||
|
@ -68,7 +68,7 @@ pub enum State {
|
|||
#[derive(Default)]
|
||||
pub struct ContextWindow {
|
||||
pub size: Vec2,
|
||||
pub text: Vec<(Rect, Indent, ContextBlock)>,
|
||||
pub text: Vec<ResolvedItem<'static>>,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub name: &'static str,
|
||||
}
|
||||
|
@ -80,16 +80,6 @@ pub struct ContextIcon {
|
|||
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>,
|
||||
hi_image: Image<'static>,
|
||||
},
|
||||
}
|
||||
|
||||
const CONTEXT_METRICS: Metrics = Metrics::new(16.0, 18.0);
|
||||
const HOVER_TIME: f64 = 0.5;
|
||||
|
||||
|
@ -231,6 +221,14 @@ const NAME_PLATE: [&str; 3] = [
|
|||
"My portfolio",
|
||||
];
|
||||
|
||||
pub struct ResolvedItem<'a> {
|
||||
pub indent: Indent,
|
||||
pub buffer: Arc<RwLock<Buffer>>,
|
||||
pub relative_bounds: Rect,
|
||||
pub url_map: Option<RangeMap<usize, Cow<'a, str>>>,
|
||||
pub image_urls: Option<(String, String)>,
|
||||
}
|
||||
|
||||
impl ContextWindow {
|
||||
pub fn set_content(
|
||||
&mut self,
|
||||
|
@ -238,119 +236,34 @@ impl ContextWindow {
|
|||
#[cfg(target_arch = "wasm32")] transform: Option<Mat2>,
|
||||
#[cfg(target_arch = "wasm32")] name: Option<&'static str>,
|
||||
font_system: &mut FontSystem,
|
||||
mut max_width: f32,
|
||||
max_width: f32,
|
||||
#[cfg(target_arch = "wasm32")] states: &mut Vec<State>,
|
||||
) {
|
||||
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(),
|
||||
let text =
|
||||
cosmic_jotdown::jotdown_into_buffers(content.iter().cloned()).collect::<Vec<_>>();
|
||||
|
||||
let (size, text) = cosmic_jotdown::resolve_paragraphs(
|
||||
&text,
|
||||
self.size,
|
||||
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 hi_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));
|
||||
let split = url.0.rsplit_once('.').unwrap();
|
||||
hi_image = Image::from_uri(format!(
|
||||
concat!(env!("PHOST"), "/{}_hi.{}"),
|
||||
split.0, split.1
|
||||
));
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
image = Image::from_uri(format!("file://assets/{}", url.0));
|
||||
let split = url.0.rsplit_once('.').unwrap();
|
||||
hi_image = Image::from_uri(format!("file://assets/{}_hi.{}", split.0, split.1));
|
||||
}
|
||||
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,
|
||||
hi_image,
|
||||
},
|
||||
None,
|
||||
1.0,
|
||||
env!("PHOST"),
|
||||
);
|
||||
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
|
||||
self.text = text
|
||||
.into_iter()
|
||||
.map(|t| ResolvedItem {
|
||||
indent: t.indent,
|
||||
buffer: Arc::new(RwLock::new(t.buffer)),
|
||||
relative_bounds: t.relative_bounds,
|
||||
url_map: t.url_map,
|
||||
image_urls: t.image_urls,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(size) = last_image_size {
|
||||
self.size.y += size.y;
|
||||
}
|
||||
self.size = size;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Some(name) = name {
|
||||
|
@ -743,113 +656,19 @@ impl eframe::App for Portfolio {
|
|||
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());
|
||||
let mut buffer = buffer.write();
|
||||
let mut editor =
|
||||
Editor::new(BufferRef::Borrowed(buffer.deref_mut()));
|
||||
if text_response.hovered()
|
||||
&& ui.input(|i| i.raw_scroll_delta == Vec2::ZERO)
|
||||
{
|
||||
let mouse_pos = ui
|
||||
.input(|i| i.pointer.latest_pos().unwrap_or_default())
|
||||
- text_rect.min.to_vec2();
|
||||
|
||||
editor.action(
|
||||
self.font_system.lock().deref_mut(),
|
||||
glyphon::Action::Click {
|
||||
x: mouse_pos.x as i32,
|
||||
y: mouse_pos.y as i32 - 3,
|
||||
},
|
||||
);
|
||||
|
||||
let mut location = editor.cursor();
|
||||
match location.affinity {
|
||||
glyphon::Affinity::After => location.index += 1,
|
||||
glyphon::Affinity::Before => {}
|
||||
}
|
||||
|
||||
if url_map.get(location.index).is_some() {
|
||||
ctx.set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
}
|
||||
}
|
||||
if text_response.clicked() && !self.image_zoomed {
|
||||
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, hi_image, ..
|
||||
} => {
|
||||
let image_rect = context_block.0.translate(rect.min.to_vec2());
|
||||
self.window.text.iter().for_each(|context_block| {
|
||||
if let Some(image_urls) = &context_block.image_urls {
|
||||
let image_rect =
|
||||
context_block.relative_bounds.translate(rect.min.to_vec2());
|
||||
|
||||
let image_response = ui.allocate_rect(image_rect, Sense::click());
|
||||
let image_response_clicked = image_response.clicked();
|
||||
|
||||
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
|
||||
{
|
||||
if image_response_clicked && !self.image_zoomed && self.zoomed {
|
||||
link_clicked = true;
|
||||
*time_offset = time;
|
||||
self.last_image_zoomed = image_response.id;
|
||||
|
@ -895,27 +714,24 @@ impl eframe::App for Portfolio {
|
|||
ui.painter().rect_filled(
|
||||
ui.max_rect(),
|
||||
Rounding::default(),
|
||||
Color32::BLACK.gamma_multiply(
|
||||
keyframe::ease_with_scaled_time(
|
||||
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,
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
|
||||
if t > 0.0 {
|
||||
hi_image.clone()
|
||||
Image::from_uri(&image_urls.1)
|
||||
} else {
|
||||
image.clone()
|
||||
Image::from_uri(&image_urls.0)
|
||||
}
|
||||
.tint(Color32::WHITE.gamma_multiply(zoom_view_opacity))
|
||||
.paint_at(&ui, image_rect.lerp_towards(&fs_rect, t));
|
||||
} else {
|
||||
image
|
||||
.clone()
|
||||
Image::from_uri(&image_urls.0)
|
||||
.tint(Color32::WHITE.gamma_multiply(zoom_view_opacity))
|
||||
.paint_at(
|
||||
ui,
|
||||
|
@ -931,6 +747,80 @@ impl eframe::App for Portfolio {
|
|||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let text_rect =
|
||||
context_block.relative_bounds.translate(rect.min.to_vec2());
|
||||
// ui.painter().debug_rect(text_rect, Color32::GREEN, "");
|
||||
if let Some(url_map) = &context_block.url_map {
|
||||
let text_response = ui.allocate_rect(text_rect, Sense::click());
|
||||
let mut buffer = context_block.buffer.write();
|
||||
let mut editor =
|
||||
Editor::new(BufferRef::Borrowed(buffer.deref_mut()));
|
||||
if text_response.hovered()
|
||||
&& ui.input(|i| i.raw_scroll_delta == Vec2::ZERO)
|
||||
{
|
||||
let mouse_pos = ui
|
||||
.input(|i| i.pointer.latest_pos().unwrap_or_default())
|
||||
- text_rect.min.to_vec2();
|
||||
|
||||
editor.action(
|
||||
self.font_system.lock().deref_mut(),
|
||||
glyphon::Action::Click {
|
||||
x: mouse_pos.x as i32,
|
||||
y: mouse_pos.y as i32 - 3,
|
||||
},
|
||||
);
|
||||
|
||||
let mut location = editor.cursor();
|
||||
match location.affinity {
|
||||
glyphon::Affinity::After => location.index += 1,
|
||||
glyphon::Affinity::Before => {}
|
||||
}
|
||||
|
||||
if url_map.get(&location.index).is_some() {
|
||||
ctx.set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
}
|
||||
}
|
||||
if text_response.clicked() && !self.image_zoomed {
|
||||
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(
|
||||
context_block.buffer.clone(),
|
||||
text_rect,
|
||||
zoom_view_opacity,
|
||||
Color::rgb(255, 255, 255),
|
||||
ui.ctx(),
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue