Add resolution support

This commit is contained in:
Isaac Mills 2024-04-19 09:42:01 -04:00
parent 3f70c53aab
commit ee9ff8e2d6
Signed by: fnmain
GPG key ID: B67D7410F33A0F61
3 changed files with 434 additions and 35 deletions

76
Cargo.lock generated
View file

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "bitflags"
version = "2.5.0"
@ -28,13 +34,28 @@ dependencies = [
"syn",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "cosmic-jotdown"
version = "0.1.0"
dependencies = [
"cosmic-text",
"emath",
"image",
"jotdown",
"log",
"nominals",
"rangemap",
"serde",
]
@ -85,6 +106,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "emath"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4c3a552cfca14630702449d35f41c84a0d15963273771c6059175a803620f3f"
[[package]]
name = "font-types"
version = "0.5.2"
@ -117,6 +144,18 @@ dependencies = [
"ttf-parser",
]
[[package]]
name = "image"
version = "0.24.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"num-traits",
]
[[package]]
name = "jotdown"
version = "0.3.2"
@ -154,19 +193,34 @@ dependencies = [
]
[[package]]
name = "proc-macro2"
version = "1.0.79"
name = "nominals"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
checksum = "bd4b6e50a0a7f2214e99ecf7f4a2c9cb9572e5817d96e37a6d31387961c23994"
[[package]]
name = "num-traits"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
]
[[package]]
name = "proc-macro2"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
@ -227,18 +281,18 @@ checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba"
[[package]]
name = "serde"
version = "1.0.197"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.197"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
dependencies = [
"proc-macro2",
"quote",
@ -273,9 +327,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.58"
version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
dependencies = [
"proc-macro2",
"quote",

View file

@ -11,7 +11,9 @@ jotdown = { git = "https://git.nations.lol/fnmain/jotdown" }
log = "0.4.21"
serde = { version = "1.0.197", features = ["derive"], optional = true }
rangemap = "1.5.1"
nominals = "0.3.0"
emath = "0.27.2"
image = { version = "0.24.9", default-features = false }
[features]
default = ["serde"]
serde = ["dep:serde", "rangemap/serde1"]

View file

@ -1,17 +1,21 @@
use std::borrow::Cow;
use cosmic_text::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, Style, Weight};
use jotdown::{Container, Event, ListKind};
use cosmic_text::{self, Align};
use cosmic_text::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, Style, Weight};
use emath::{Pos2, Rect, Vec2};
use jotdown::{Container, Event, ListKind, OrderedListNumbering, OrderedListStyle};
use nominals::{LetterLower, LetterUpper, Nominal, RomanLower, RomanUpper};
pub use jotdown;
use rangemap::RangeMap;
#[cfg(feature = "serde")]
mod serde_suck;
pub mod serde_suck;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "serde")]
pub use serde_suck::*;
use rangemap::RangeMap;
pub struct JotdownBufferIter<'a, T: Iterator<Item = Event<'a>>> {
pub djot: T,
pub indent: Vec<Indent>,
@ -27,9 +31,10 @@ struct JotdownIntoBuffer<'a, 'b, T: Iterator<Item = Event<'a>>> {
pub urls: Vec<(std::ops::Range<usize>, Cow<'a, str>)>,
pub location: usize,
pub added: bool,
pub top_level_container: Option<Container<'a>>,
}
#[derive(Default, Clone, Copy)]
#[derive(Default, Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct Indent {
#[cfg_attr(feature = "serde", serde(with = "ListKindOption"))]
@ -37,7 +42,7 @@ pub struct Indent {
pub indent: f32,
}
pub const INDENT_AMOUNT: f32 = 18.0;
pub const INDENT_AMOUNT: f32 = 64.0;
impl<'a, 'b, T: Iterator<Item = Event<'a>>> Iterator for JotdownIntoBuffer<'a, 'b, T> {
type Item = (&'a str, Attrs<'static>);
@ -92,17 +97,32 @@ impl<'a, 'b, T: Iterator<Item = Event<'a>>> Iterator for JotdownIntoBuffer<'a, '
}
Event::Start(container, _) => match container {
Container::Heading { level, .. } => {
self.metrics = Metrics::new(4.0 - level as f32, 4.0 - level as f32);
let l = match level {
1 => 2.0,
2 => 1.5,
3 => 1.17,
4 => 1.0,
5 => 0.83,
_ => 0.67,
};
self.metrics = Metrics::new(l, l * 1.1);
self.top_level_container.get_or_insert(container);
}
Container::Emphasis => self.attrs = self.attrs.style(Style::Italic),
Container::Strong => self.attrs = self.attrs.weight(Weight::BOLD),
Container::Verbatim => self.attrs = self.attrs.family(Family::Monospace),
Container::ListItem { .. } | Container::Paragraph => {
self.top_level_container.get_or_insert(container);
}
Container::List { kind, .. } => self.indent.push(Indent {
indent: self.indent.last().copied().unwrap_or_default().indent
+ INDENT_AMOUNT,
modifier: Some(kind),
}),
Container::Image(url, _) => self.image_url = Some(url),
Container::Image(ref url, _) => {
self.image_url = Some(url.clone());
self.top_level_container.get_or_insert(container);
}
Container::Link(_, _) => {
self.link_start = self.location + 1;
self.attrs = self.attrs.color(Color::rgb(96, 198, 233));
@ -116,7 +136,7 @@ impl<'a, 'b, T: Iterator<Item = Event<'a>>> Iterator for JotdownIntoBuffer<'a, '
Container::List { .. } => {
self.indent.pop();
}
Container::Heading { .. } | Container::Paragraph | Container::Image(_, _) => {
Container::Heading { .. } | Container::Paragraph => {
if self.added {
return None;
}
@ -138,20 +158,14 @@ impl<'a, 'b, T: Iterator<Item = Event<'a>>> Iterator for JotdownIntoBuffer<'a, '
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct RichText<'a>(
Cow<'a, str>,
#[cfg_attr(feature = "serde", serde(with = "AttrsSerde"))]
#[cfg_attr(feature = "serde", serde(borrow))]
Attrs<'a>,
#[cfg_attr(feature = "serde", serde(borrow))] pub Cow<'a, str>,
#[cfg_attr(feature = "serde", serde(with = "AttrsSerde"))] pub Attrs<'a>,
);
impl<'a> From<(Cow<'a, str>, Attrs<'a>)> for RichText<'a> {
fn from(value: (Cow<'a, str>, Attrs<'a>)) -> Self {
Self(value.0, value.1)
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct JotdownItem<'a> {
pub indent: Indent,
@ -160,15 +174,235 @@ pub struct JotdownItem<'a> {
#[cfg_attr(feature = "serde", serde(with = "MetricsSerde"))]
pub metrics: Metrics,
pub image_url: Option<Cow<'a, str>>,
pub margin: f32,
pub url_map: Option<RangeMap<usize, Cow<'a, str>>>,
}
#[derive(Clone)]
pub struct ResolvedJotdownItem<'a> {
pub indent: Indent,
pub buffer: Buffer,
pub metrics: Metrics,
pub relative_bounds: Rect,
pub url_map: Option<RangeMap<usize, Cow<'a, str>>>,
pub image_urls: Option<(String, String)>,
}
pub fn resolve_paragraphs<'a>(
paragaphs: &[JotdownItem<'a>],
viewbox: Vec2,
font_system: &mut FontSystem,
metrics: Metrics,
align: Option<Align>,
factor: f32,
base_uri: &'static str,
) -> (Vec2, Vec<ResolvedJotdownItem<'a>>) {
let mut size = Vec2::new(0.0, 0.0);
// Cannot be f32::MAX
let mut last_margin = 99999.0;
let mut last_image_size: Option<Vec2> = None;
let mut first = true;
let mut in_list = false;
let mut list_number = 0;
let mut paragraphs = paragaphs
.iter()
.flat_map(|jbuffer| {
let mut buffer =
jbuffer
.clone()
.resolve(font_system, viewbox.x, metrics, align, factor);
let margin = jbuffer.margin * buffer.metrics.line_height;
let margin_top = (margin - last_margin).max(0.0);
buffer.relative_bounds = buffer
.relative_bounds
.translate(Vec2::new(buffer.indent.indent, size.y + margin_top));
let buffer_size = buffer.relative_bounds.size();
let buffer_indent = buffer.indent.indent;
let result_buffers = if let Some(url) = &jbuffer.image_url {
let url = url.split_once('#').unwrap();
let image_size = url.1.split_once('x').unwrap();
let image_size =
Vec2::new(image_size.0.parse().unwrap(), image_size.1.parse().unwrap());
let image = format!("{}/{}", base_uri, url.0);
let split = url.0.rsplit_once('.').unwrap();
let hi_image = format!("{}/{}_hi.{}", base_uri, split.0, split.1);
buffer.image_urls = Some((image, hi_image));
buffer.relative_bounds = Rect::from_min_size(
Pos2::new(buffer.indent.indent, size.y + margin_top),
image_size,
);
const IMAGE_PADDING: f32 = 8.0;
if let Some(last_size) = last_image_size.as_mut() {
let ls = *last_size;
last_size.x += image_size.x + IMAGE_PADDING;
if last_size.x > viewbox.x {
size.y += last_size.y + margin_top;
last_size.x = image_size.x + IMAGE_PADDING;
last_size.y = image_size.y + margin_top;
buffer.relative_bounds = Rect::from_min_size(
Pos2::new(buffer.indent.indent, size.y + margin_top),
image_size,
);
} else {
last_size.y = last_size.y.max(image_size.y + margin_top);
buffer.relative_bounds =
buffer.relative_bounds.translate(Vec2::new(ls.x, 0.0));
}
} else {
if image_size.x > viewbox.x {
let max_size = Vec2::new(viewbox.x, image_size.y);
let new_size = scale_to_fit(image_size, max_size, true);
buffer.relative_bounds = Rect::from_min_size(
Pos2::new(buffer.indent.indent, size.y + margin_top),
new_size,
);
size.y += new_size.y + margin_top;
} else {
last_image_size = Some(image_size + Vec2::new(IMAGE_PADDING, margin_top));
}
}
[Some(buffer), None]
} else if let Some(mut list_kind) = buffer.indent.modifier {
if let Some(image_size) = last_image_size {
size.y += image_size.y;
}
if let ListKind::Ordered { start, .. } = &mut list_kind {
if !in_list {
list_number = *start;
} else {
list_number += 1;
}
*start = list_number;
}
in_list = true;
let mut list_buffer = Buffer::new(font_system, buffer.metrics);
list_buffer.set_text(
font_system,
make_list_number(list_kind).as_ref(),
Attrs::new().family(Family::SansSerif),
Shaping::Advanced,
);
list_buffer.set_wrap(font_system, cosmic_text::Wrap::WordOrGlyph);
list_buffer.set_size(font_system, f32::MAX, f32::MAX);
list_buffer.shape_until_scroll(font_system, false);
let list_buffer_metrics = buffer.metrics;
let indent = (buffer_indent) - (INDENT_AMOUNT * factor);
[
Some(buffer),
Some(ResolvedJotdownItem {
indent: Indent {
modifier: None,
indent,
},
relative_bounds: measure_buffer(
&list_buffer,
Vec2::new(f32::MAX, f32::MAX),
)
.translate(Vec2::new(indent, size.y + margin_top)),
buffer: list_buffer,
metrics: list_buffer_metrics,
url_map: None,
image_urls: None,
}),
]
} else {
if let Some(image_size) = last_image_size {
size.y += image_size.y;
}
in_list = false;
[Some(buffer), None]
};
size.y += buffer_size.y + (margin_top + margin);
last_margin = margin;
first = false;
size.x = size.x.max(buffer_size.x + buffer_indent);
result_buffers
})
.filter_map(|p| p)
.collect::<Vec<_>>();
size.y -= last_margin;
if let Some(image_size) = last_image_size {
size.y += image_size.y;
}
paragraphs.iter_mut().for_each(|paragraph| {
if paragraph.image_urls.is_none() {
paragraph
.relative_bounds
.set_width(size.x - paragraph.indent.indent);
paragraph
.buffer
.set_size(font_system, size.x - paragraph.indent.indent, f32::MAX);
paragraph.buffer.shape_until_scroll(font_system, false);
}
});
(size, paragraphs)
}
impl<'a> JotdownItem<'a> {
pub fn resolve(
mut self,
font_system: &mut FontSystem,
width: f32,
metrics: Metrics,
align: Option<Align>,
factor: f32,
) -> ResolvedJotdownItem<'a> {
self.indent.indent *= factor;
let buffer = self.make_buffer(font_system, width, metrics, align);
ResolvedJotdownItem {
indent: self.indent,
relative_bounds: measure_buffer(&buffer, Vec2::new(width, f32::MAX)),
buffer,
metrics: Metrics::new(
self.metrics.font_size * metrics.font_size,
self.metrics.line_height * metrics.line_height,
),
url_map: self.url_map,
image_urls: None,
}
}
pub fn new_default(text: Vec<RichText<'a>>) -> Self {
Self {
indent: Indent {
modifier: None,
indent: 0.0,
},
buffer: text,
metrics: Metrics::new(1.0, 1.1),
url_map: None,
margin: 0.0,
image_url: None,
}
}
}
impl<'a> JotdownItem<'a> {
pub fn make_buffer(
&self,
font_system: &mut FontSystem,
width: f32,
metrics: Metrics,
align: Option<Align>,
) -> Buffer {
let mut buffer = Buffer::new(
font_system,
@ -186,6 +420,11 @@ impl<'a> JotdownItem<'a> {
buffer.set_wrap(font_system, cosmic_text::Wrap::WordOrGlyph);
buffer.set_size(font_system, width - self.indent.indent, f32::MAX);
for line in &mut buffer.lines {
line.set_align(align);
}
buffer.shape_until_scroll(font_system, false);
buffer
}
@ -200,19 +439,21 @@ impl<'a, T: Iterator<Item = Event<'a>>> Iterator for JotdownBufferIter<'a, T> {
attrs: Attrs::new().family(Family::SansSerif),
indent: &mut self.indent,
image_url: None,
metrics: Metrics::new(1.0, 1.0),
metrics: Metrics::new(1.0, 1.1),
added: false,
link_start: 0,
location: 0,
urls: Vec::new(),
top_level_container: None,
};
let buffer = (&mut jot)
.map(|r| RichText(Cow::Borrowed(r.0), r.1))
.map(|r| RichText(r.0.into(), r.1))
.collect::<Vec<_>>();
let image_url = jot.image_url;
let urls = jot.urls;
let top_level_containers = jot.top_level_container;
let added = jot.added;
let metrics = jot.metrics;
let indent = self.indent.last().copied().unwrap_or_default();
@ -225,12 +466,29 @@ impl<'a, T: Iterator<Item = Event<'a>>> Iterator for JotdownBufferIter<'a, T> {
None
} else {
let mut map = RangeMap::new();
map.extend(urls.into_iter());
map.extend(
urls.into_iter()
.map(|(range, url)| (range.start..range.end + 1, url)),
);
Some(map)
},
buffer,
image_url,
metrics,
margin: match top_level_containers {
Some(Container::Heading { level, .. }) => match level {
1 => 0.34,
2 => 0.42,
3 => 0.5,
4 => 0.65,
5 => 0.85,
6 => 1.25,
_ => unreachable!(),
},
Some(Container::ListItem) => 0.5,
Some(Container::Image(_, _)) => 1.0,
_ => 1.0,
},
});
}
}
@ -244,3 +502,88 @@ pub fn jotdown_into_buffers<'a, T: Iterator<Item = Event<'a>>>(
indent: Vec::new(),
}
}
pub fn make_list_number(list_kind: ListKind) -> Cow<'static, str> {
match list_kind {
ListKind::Unordered | ListKind::Task => Cow::Borrowed(""),
ListKind::Ordered {
numbering,
style,
start: number,
} => {
use std::fmt::Write;
let mut result = String::new();
if matches!(style, OrderedListStyle::ParenParen) {
result.push('(');
}
match numbering {
OrderedListNumbering::Decimal => {
result.write_fmt(format_args!("{}", number)).unwrap()
}
OrderedListNumbering::AlphaLower => {
result
.write_fmt(format_args!("{}", (number - 1).to_nominal(&LetterLower)))
.unwrap();
}
OrderedListNumbering::AlphaUpper => {
result
.write_fmt(format_args!("{}", (number - 1).to_nominal(&LetterUpper)))
.unwrap();
}
OrderedListNumbering::RomanLower => {
result
.write_fmt(format_args!("{}", number.to_nominal(&RomanLower)))
.unwrap();
}
OrderedListNumbering::RomanUpper => {
result
.write_fmt(format_args!("{}", number.to_nominal(&RomanUpper)))
.unwrap();
}
}
match style {
OrderedListStyle::Period => result.push('.'),
OrderedListStyle::Paren | OrderedListStyle::ParenParen => result.push(')'),
}
Cow::Owned(result)
}
}
}
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),
);
Rect::from_min_size(Pos2::ZERO, size)
}
fn scale_to_fit(image_size: Vec2, available_size: Vec2, maintain_aspect_ratio: bool) -> Vec2 {
if maintain_aspect_ratio {
let ratio_x = available_size.x / image_size.x;
let ratio_y = available_size.y / image_size.y;
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
let ratio = if ratio.is_finite() { ratio } else { 1.0 };
image_size * ratio
} else {
available_size
}
}